Java基础-一文搞懂泛型
什么是泛型?为什么要用泛型?
泛型允许在定义类、接口和方法的时候,把 类型 作为参数。能用不同的输入来重用相同的代码
优点: 也就是为什么要用泛型
泛型是在编译器起作用的,在运行期是不存在泛型的。
-
Java 编译器会对泛型代码进行严格的类型检查,如果违反类型安全就会报错。修复编译期错误肯定比修复运行期错误更容易
List<String> list = new ArrayList<>(); list.add("abc"); // 正常运行 list.add(123);比较智能的编译器 IDEA 会直接提示
Required type: String Provided: int,然后无法运行。
如果你使用javac去编译这个类,报错如下
-
消除强制类型转换
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); // 需要手动转换为 String注意:这里因为如果不指定
List泛型类型,默认就是Object,因为Object是String的父类,所以往list添加字符串是可以成功的,而强转为String是在运行期去做的,它发现这个地址里存的确实是String类型,然后才能正确强转成功,如果不是这个类型,就报错抛出异常所以,比较恐怖的是下面这段代码竟然可以编译成功!
List list = new ArrayList<>(); list.add("abc"); // 正常运行 Integer a = (Integer) list.get(0);
但是在运行期就会报错ClassCastException。所以也体现了第一点的好处,类型检查。下面用一下泛型,你会发现代码不用显示的去转换了
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); -
可以实现泛型算法
程序员可以用泛型去自定义自己的方法,类型安全并且更容易阅读
泛型
简单的 Box 类
先演示一个非泛型的 Box 类,可以对任何类型的对象进行操作
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
这样确实可以实现传入任何数据类型,但是也有安全问题!
Box box = new Box();
box.set(new Integer(1)); // B 放入的
box.set("ab"); // A 放入的
// B 又去取,因为是 Integer,但实际是 String
Integer i = (Integer) box.get(); // ClassCastException
关键是这个在编译期间是不知道的,只有在运行时才能发现错误。
泛型版本的 Box 类
泛型类定义格式如下
class name<T1, T2, ..., Tn> {}
在类名后面加上泛型集合,<T1, T2, ... Tn>,用逗号分隔,每个符号都代表一个不同的类型
重写一下 Box 类
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
所有的 Object 都被替换为了 T。在使用这个类的时候可以传入任何你想用的类型,可以是数组、接口、任何类,但是不能是基本变量哈
类名参数命名约定
按照惯例,通常为单个大写字母(不是强制的,但是这样很专业)
常见的类型参数名称有
- E 代表元素(Java集合框架常用的)
- K 键
- N 数值 --> 比如
<N extends Number>,通常来表示数字相关的 - T 类型
- V 值
- S,U,V 第二三四种类型,T是第一种
调用和实例化泛型类型
要用泛型,必须把 T 替换为某个具体值,想实例化该类,还是用 new,但是要加上类型参数
Box<Integer> integerBox = new Box<Integer>();
按照正常写法,这种才是最标准的(都没有省略 Integer)
菱形
Java 7 之后,只要编译器能从上下文中推断出类型参数,就可以用 <> 来替换泛型类构造函数需要的类型参数
Box<Integer> integerBox = new Box<>();
在类型推断里面再详细说
多个类型参数
泛型类可以有多个类型参数,比如下面这个实现泛型 Pair 接口的 OrderedPair 类
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
调用的时候就是
Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
Pair<String, String> p2 = new OrderedPair<>("hello", "world");
8自动装箱为Integer- 菱形语法
原始类型
也就是 Raw Types,含义是泛型类使用时不给类型参数指定任何类型实参(T 就是类型参数,Box<Integer> 中的 Integer 就是类型实参)
public class Box<T> {}
Box<Integer> intBox = new Box<>(); // 指定了类型实参
Box rawBox = new Box(); // 没指定类型实参,即原始类型
Box 是泛型类型 Box<T> 的原始类型
JDK 5 之前还没有泛型,当时就是用的原始类型,Box 返回的就是 Object,所以现在为了兼容,允许把泛型类型赋值给原始类型
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // 没问题
但是如果把原始类型赋值给泛型类型会有警告
Box box = new Box<>();
Box<String> rawBox = box; // Unchecked assignment: 'Box' to 'Box<java.lang.String>'

并且原始类型调用泛型类型中定义的泛型方法,也会有警告
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // Unchecked call to 'set(T)' as a member of raw type 'Box'
这是啥含义?原始类型绕过了泛型类型检查!!!编译是可以通过的,但是运行时就会出现问题!所以应该避免使用原始类型
泛型方法
泛型方法是说方法本身引入了类型参数,这个类型参数的作用域就仅限于这个方法了。
允许有静态和非静态方法以及泛型类构造函数。
对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
调用方法如下
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
// boolean same = Util.<Integer, String>compare(p1, p2);
boolean same = Util.compare(p1, p2);
这里可以省略 .<Integer, String> 是因为有类型推断
{% note danger %}
静态方法里可以用类上声明的泛型参数吗?
class Box<T> {
private T object;
public static T getObject() { // 编译错误
return null;
}
}
为什么?原因很简单,因为 T 是跟着对象走的,静态方法是跟着类走的。T 什么时候用?是在类实例化的时候
Box<String> box = new Box<>();
而静态方法在类加载的时候就有了,所以用不了
{% endnote %}
大部分都是演示的静态方法里用的自己的类型参数,但是其实成员方法和构造方法一样可以独立有自己的类型参数
class Box<T> {
private T object;
public <U> Box(U input) {
System.out.println(input);
}
public <U> U convert(U input) {
return input;
}
}
⚠️:虽然构造函数和成员方法里面都用的 U,但是前面说了,这个类型参数的作用域仅在自己方法内,所以类型不是必须一致的,也就是下面这种,构造函数的 U 是 String 类型,convert 方法里面的 U 是 Double 类型
调用
Box<Integer> box = new <String> Box<Integer>("abc"); // String 是构造函数上的类型参数 U,Integer 是类上的类型参数 T
Double abc = box.<Double>convert(2.1);
// 使用类型推断可以简略写
Box<Integer> box = new Box<>("abc"); // 根据输入的参数 "abc" 判断 U 为 String
Double abc = box.convert(2.1);
有界类型参数
单个约束
现在我们用泛型类型实参可以是任意对象类型,但是如果我们想加个限制怎么做?比如我只想接受 Number 类型还有它子类的实例?
使用 <N extends Number> 的方式,也就是 extends 类/接口(和继承是同一个关键字,但是用法不同)
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <N extends Number> void inspect(N n){
System.out.println("T: " + t.getClass().getName());
System.out.println("N: " + n.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // 不对,因为 String 类型不是 Number 的子类
}
}
既然知道了这个类的上界,其实就可以用这个上界里面的方法了!比如可以调用 Number 类中的一些方法。
我们原来的
public class Box<T> {}
相当于
public class Box<T extends Object> {}
多个约束
上面那个是单个约束的情况,其实也可以有多个约束
<T extends B1 & B2 & B3>
具有多个约束的 类型变量 要同时满足是这些约束的子类型。比如 T 我既要是 B1 的子类型,也要是 B2 的子类型,也是 B3 的子类型
如果其中一个约束是类,那必须放在第一个位置。
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
class D <T extends B & A & C> { /* ... */ } // compile-time error
泛型方法与有界类型参数
有界类型参数是实现泛型算法的关键。考虑以下方法,它用于统计数组 T[] 中大于指定元素 elem 的元素个数。
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
不加上界的话,T 相当于 T extends Object,所以编译器只能保证 T 有 Object 的能力,你只能调用 Object 能提供的方法
大于运算符 (>) 仅适用于 short、int、double、long、float、byte 和 char 等基本类型,所以这里没办法进行比较,解决办法是由 Comparable<T> 接口限定类型的类型参数
public interface Comparable<T> {
public int compareTo(T o);
}
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
{% note danger no-icon %}<T extends Comparable<T>> 和 <T extends Comparable> 的区别<T extends Comparable<T>> 表示 T 能和 T 类型比较,类型安全,推荐。
<T extends Comparable> 表示 T 实现了原始 Comparable,类型检查变弱,不推荐。也就是说它把接口中的 compareTo(T o) 退化成了 compareTo(Object o)。
比如
class MyValue implements Comparable<String> {
@Override
public int compareTo(String other) {
return 0;
}
}
代表 MyValue 可以和 String 比较,但是 MyValue 和 MyValue 没有比较方法,所以如果是 <T extends Comparable>,这个类是可以作为泛型实参的!!!但是 <T extends Comparable<T>> 是会拒绝的,编译都不通过,只能是 MyValue implements Comparable<MyValue> 才能通过。
所以 <T extends Comparable> 只保证了 T 有比较能力,但是和谁比的不知道,<T extends Comparable<T>> 保证了 T 和 T 之间有比较能力。
{% endnote %}
泛型,继承,子类
我们知道,类型只要兼容,就可以把一种类型的对象赋值给另一种类型的对象,比如
String a = "abc";
Object obj = a; // Object 是 String 的超类
List list = new ArrayList(); // ArrayList 实现了 List 接口
所以如下是允许的
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
因此同样对于泛型也是允许的,因为添加的对象都是 Number 子类
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
那比如我们有这样一个方法
class Box<T> {
private T t;
public static void boxTest(Box<Number> n) {}
}
下面情况会如何?
Box<Integer> integerBox = new Box<>();
Box<Number> numberBox = new Box<>();
Box box = new Box<>();
Box.boxTest(integerBox); // Required type: Box<Number> Provided: Box<Integer>
Box.boxTest(numberBox);
Box.boxTest(box); // Unchecked assignment
不能传入 Box<Integer>,因为 Box<Integer> 并非 Box<Number> 的子类型

所以,给定两个具体类型 A 和 B(例如 Number 和 Integer),无论 A 和 B 之间是否相关,MyClass<A> 与 MyClass<B> 之间都没有关系。MyClass<A> 和 MyClass<B> 的共同父类是 Object。
泛型类与子类
泛型类或泛型接口也可以通过 extends 或 implements 被继承或实现,从而形成子类型关系。两个类或接口之间的类型参数如何对应,取决于 extends 和 implements 后面的声明方式。
比如 Collection<E> 和 List<E>、ArrayList<E> 的关系
public interface Collection<E> extends Iterable<E> {}
public interface List<E> extends Collection<E> {}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
但是注意,ArrayList<String> 是 List<String> 的子类型,但是 ArrayList<String> 不是 List<Integer> 的子类型。因为根据源码可以知道 ArrayList<String> 相当于是实现了 List<String> 的,你不能 List<Integer> list = new ArrayList<String>();,只能 List<String> list = new ArrayList<String>();, List<Integer> 和 List<String> 是不一样的!!!
然后看下面这种
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
我们可以知道下面这三种其实都算是 List<String> 的子类!因为第二个类型参数和 List<E> 没有关系
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>

类型推断
类型推断是 Java 编译器的一种能力。编译器会根据方法声明、实际传入的参数,以及赋值或方法返回值的目标类型,推断泛型方法中的类型实参,并尽量选择一个能满足所有条件的最具体类型。
比如
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
传递的参数是 (String, ArrayList),返回值类型是 Serializable,方法声明里面都是 T 一种类型,而 String 和 ArrayList 都实现了 Serializable,所以最合适的类型是 Serializable。
类型推断与泛型方法
前面我们也了解了,类型推断可以让我们像调用普通方法一样调用泛型方法,不需要在尖括号里面指定类型,比如下面代码
public class BoxDemo {
public static <U> void addBox(U u, List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
ArrayList<Box<Integer>> listOfIntegerBoxes = new ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); // 可以不用加 <Integer>
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes); // 通过第一个参数可以推断出来泛型类型
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
泛型方法 addBox 定义了一个名为 U 的类型参数。通常,Java 编译器可以推断泛型方法调用的类型参数。所以在大多数情况下,不需要指定它们。
类型推断与泛型实例化
只要编译器能够从上下文中推断出类型实参,就可以用一组空的类型参数 <> 来替代调用泛型类构造方法时所需的类型实参。这对尖括号通常被非正式地称为“菱形”。
Map<String, List<String>> myMap = new HashMap<String, List<String>>(); // 两次用到 <String, List<String>>
Map<String, List<String>> myMap = new HashMap<>(); // 简化,用一个空的 <> 来替代
Map<String, List<String>> myMap = new HashMap(); // Unchecked assignment: 'java.util.HashMap' to 'java.util.Map<java.lang.String,java.util.List<java.lang.String>>'
但是你不能不写 <>,如果不写代表是原始类型!编译是可以通过的,但是运行的时候 可能 出现问题
泛型类和非泛型类中的类型推断与泛型构造方法
构造方法可以是泛型的,也就是说无论这个类是不是泛型的,构造方法都可以声明自己的类型参数
class MyClass<X> {
public <T> MyClass(T t) {
// ...
}
}
使用
MyClass<Integer> myObject = new MyClass<>(""); // 推断出 T 为 String, X 为 Integer
目标类型
Java 编译器会利用目标类型来推断泛型方法调用的类型参数。一个表达式的目标类型,是 Java 编译器根据该表达式出现的位置所期望的数据类型。比如 Collections.emptyList
// 定义
static <T> List<T> emptyList();
进行赋值的时候
List<String> listOne = Collections.emptyList();
编译器推断出类型实参 T 必须是 String。
通配符
在泛型代码中,问号 ? 被称为通配符,表示一个未知类型。通配符可以用于多种情况:作为参数、字段或局部变量的类型;有时也可以作为返回类型,尽管更好的编程实践是使用更具体的类型。通配符绝不会被用作泛型方法调用、泛型类实例创建或父类型中的类型实参。
{% note warning %}? 是一种通配符类型实参,但它不是类型参数,也不是对 T 的扩展,和 T 没有直接关系。
{% endnote %}
上界通配符
比如你有一个方法,想可以处理 List<Integer>、List<Double>、List<Number>,可以通过使用上界通配符来实现。(你用泛型是没办法实现的 void get(T t) 只能接收一种类型)
class Util {
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
}
调用
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5);
System.out.println(Util.sumOfList(integerList)); // 15.0
System.out.println(Util.sumOfList(doubleList)); // 16.5
前面也提到过,如果你的方法签名是
public static double sumOfList(List<Number> list) {}
其实你传递的参数就只能是 List<Number>,传递 List<Integer> 和 List<Double> 都会报错的。List<Number> 只匹配 Number 类型的列表,List<? extends Number> 匹配 Number 类型或它的任意子类类型的列表。
无界通配符
无界通配符类型使用通配符字符 ? 来指定,例如 List<?>。这被称为未知类型的列表。无界通配符在以下两种场景中很有用:
- 正在编写的方法可以使用
Object类中提供的功能来实现。 - 当代码使用泛型类中不依赖类型参数的方法时。例如,
List.size或List.clear。事实上,Class<?>经常被使用,就是因为Class<T>中的大多数方法并不依赖于T。
比如我们想实现一个打印任意类型的列表方法
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
这个方法实现不了,它只能打印 List<Object> 类型的,因为 List<Integer> 不属于它的子类。所以要编写一个通用的方法,就是 List<?>
public static void printList(List<?> list) {}
这样对于任意具体类型 A,List<A> 都是 List<?> 的子类型,所以你可以用这个方法来打印任意类型的列表
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
{% note warning %}List<Object> 和 List<?> 并不相同。你可以向 List<Object> 中插入一个 Object,或者 Object 的任意子类型。但是你只能向 List<?> 中插入 null。
{% endnote %}
这是为什么?
我们假设可以插入
public static void method1(List<?> list) {
list.add("1");
list.add(2);
}
? 表示未知类型,但是真正调用 method1 的地方可能传入的是 List<Integer>,也可能是 List<String>,如果允许插入,那么必然出现异常。所以对于 method1 来说,? 是未知类型,这个方法可不知道你要加什么类型,也就是说编译器不知道 ? 到底是什么,只能禁止添加具体对象。
下界通配符
下界通配符使用通配符字符 ? 表示,后面跟着 super 关键字,再后面跟着它的下界:<? super A>
{% note warning %}
可以用通配符指定上界,也可以指定下界,但是不能同时指定两者
{% endnote %}
假设你想编写一个方法,把 Integer 对象放入一个列表。为了最大化灵活性,你希望这个方法可以用于 List<Integer>、List<Number> 和 List<Object>——也就是任何能够容纳 Integer 值的列表。那就需要指定 List<? super Integer>
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
Object o = list.get(0);
}
所以如果是读取的话,比如 list.get(0) 只能安全的变为 Object,因为 Integer 父类最大的是 Object,但是又不能确定具体是哪个,所以如果你想用 Integer 或者 Number 只能强转!!!
所以按照这么来说,总结一下就是
List<? extends Number>能接收Number及其子类的List,适合读,虽然不知道具体什么类型,但是都能当成Number来读,但是不适合写,因为不知道这个List读真实元素类型是Number的哪一个子类,所以没办法安全的去添加Integer、Double这些具体对象,唯一能添加的是null。List<? super Integer>能接收Integer及其父类的List,适合写,因为不管接收的List<? ...>是啥类型,都一定能放Integer,但是读取的时候就不能确定是谁了,可能是Integer、Number、Object,所以为了安全,读出来只能当Object,想用就自己强转。
{% note warning %}
有<? super A>、<? extends A>,有T extends A,但是没有T super A哦!!因为这个很矛盾,本来T是一个泛型,加了super A,就代表Object也是A的父类,意味着编译器不确定你这个T不知道是什么范围了,只敢让你用Object的方法,那就失去了泛型出现的初衷,就是为了限制范围来获取特定的能力。
{% endnote %}
通配符与子类型
前面说过,泛型类或接口之间不会仅仅因为它们的类型之间存在关系而产生关系,比如 List<Integer> 不是 List<Number> 的子类。不过,可以使用通配符在泛型类或接口之间创建关系
那既然这样,List<Integer> 和 List<Number> 到底什么关系呢?

List<Number> 和 List<Integer> 的共同父类型是未知类型的列表。即 List<?>。
为了在这些类之间创建一种关系,使代码能够通过 List<Integer> 的元素访问 Number 的方法,可以使用上界通配符:
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK。List<? extends Integer> 是 List<? extends Number> 的子类型
因为 Integer 是 Number 的子类型,并且 numList 是一个 Number 对象的列表,所以现在 intList(一个 Integer 对象的列表)和 numList 之间存在了一种关系。下面的图展示了使用上界通配符和下界通配符声明的几个 List 类之间的关系。

{% note info %}
这个关系你可以用类型的可接受范围来记忆
比如 List<? extends Number> 能接收 Number 及其子类,List<? extends Integer> 是接收 Integer 及其子类,范围更小。List<? super Integer> 是 Integer 及其父类,和 List<? extends Number> 没有完全包含的关系,所以他们只有公共的父类 List<?>,相互之间没关系
{% endnote %}
通配符捕获和辅助方法
在某些情况下,编译器会推断通配符的类型。例如,一个列表可能被定义为 List<?>,但是在计算表达式时,编译器会从代码中推断出一个特定类型。这种场景称为通配符捕获。
class WildcardError {
void foo(List<?> i) {
// Object o = i.get(0);
// i.set(0, o);
// 等价下面
i.set(0, i.get(0)); // 不行,因为 set 需要的是那个未知类型,而不是任意 Object
}
}
为什么有问题呢?因为 i.get(0) 读出来,编译器并不知道什么类型,只能选择最安全的方式,是 Object,但是在 i.set(0, o); 时,List<?> 里面存放的应该是指定的未知类型,而不是 Object!!!所以编译器认为无法插入,所以会报错。比如我传进来 List<String>,不能你读一次变成 Object 又给我插进去了啊。

编译会报错,如上
这里的 CAP#1 是编译器临时起的名字,代表捕获到的某个未知类型,编译器知道这里有个未知类型,但是具体是啥不知道
{% note info %}? 是“匿名的未知类型”T 是“有名字的未知类型”
{% endnote %}
但是这个操作明明是正确的,怎么绕过编译器呢?可以编写一个捕获通配符的私有辅助方法来修复它,通常命名规则是 原方法名Helper
class WildcardError {
void foo(List<?> i) {
fooHelper(i);
}
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
借助辅助方法,编译器在调用时会使用推断来确定 T 是 CAP#1,也就是捕获变量。
原来 List<?> 里的 ? 是一个匿名的未知类型。编译器在处理它时,会把这个未知类型捕获成一个临时类型,比如 CAP#1。
对于 i.set(0, ...) 来说,它真正需要的参数类型不是 Object,而是这个被捕获的未知类型 CAP#1。
但是在 foo(List<?> i) 里,? 没有名字,编译器不能方便地把 i.get(0) 的结果作为 set 所需的同一个类型来处理。
所以通过辅助方法 fooHelper(List<T> l),让编译器把 ? 捕获成方法自己的类型参数 T。调用 fooHelper(i) 时,可以理解成这次调用中 T = CAP#1,于是 List<T> 就相当于 List<CAP#1>。
进入方法体后,l.get(0) 返回 T,l.set(0, T) 也需要 T,所以 l.set(0, l.get(0)) 可以通过编译。
通配符使用指南
这节说一下什么时候使用上界通配符,什么时候使用下界通配符。可以对变量这么理解,假设一个带有两个参数的 copy 方法:copy(src, dest),src 要提供要复制的数据,所以它是 in 参数,dest 要保存数据,供其他地方使用,是 out 变量,可以使用 in 和 out原则来决定是否使用通配符,以及使用哪种类型的通配符是合适的。
- In 变量使用上界通配符定义,使用
extends关键字。 - Out 变量使用下界通配符定义,使用
super关键字。 - 如果 In 变量可以使用
Object类中定义的方法来访问,则使用无界通配符。 - 如果代码需要同时把变量作为 In 和 Out 变量来访问,则不要使用通配符。
同时要避免通配符作为返回类型。
前面也说了 List<? extends A> 只能进行读,没办法写入(能写入 null),下面再补几种能做的操作
- 可以
clear()。也就是List.clear()来清空列表,因为不需要提供具体类型的元素,所以可以 - 可以通过迭代器删除。
List.iterator(),删除也不需要提供新元素,所以可以 - 可以通过辅助方法读出来写进去,前面已经提过
类型擦除
泛型被引入 Java 语言,是为了在编译时提供更严格的类型检查,并支持泛型编程。
为了实现泛型,Java 编译器会应用类型擦除,它会:
- 将泛型类型中的所有类型参数替换为它们的边界;如果类型参数是无界的,则替换为
Object。因此,生成的字节码只包含普通的类、接口和方法。 - 在必要时插入类型转换,以保持类型安全。
- 生成桥接方法,以在扩展泛型类型时保持多态性。
类型擦除确保不会为参数化类型创建新的类;因此,泛型不会带来运行时开销。
泛型类型的擦除
在类型擦除过程中,Java 编译器会擦除所有类型参数,并且如果类型参数是有界的,就用它的第一个边界替换;如果类型参数是无界的,就用 Object 替换。
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
因为 T 是无界的,所以编译器会用 Object 替换它
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
再来演示一个有界的
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
擦除后,用第一个边界类 Comparable 替换有界类型参数 T
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
{% note info %}
参数化类型会擦成它的原始类型(还保留泛型的话不就没擦干净吗)
Node<T> -> 擦成原始类型 Node,而不是 Node<Comparable>
Comparable<T> -> 擦成原始类型 Comparable
{% endnote %}
泛型方法的擦除
也会擦除泛型方法参数中的类型参数
// 统计 elem 在 anArray 中出现的次数。
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
因为 T 无界,所以用 Object 替换它
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
假设有继承规则为
就可以定一个泛型方法来绘制不同的形状
public static <T extends Shape> void draw(T shape) {}
擦除之后就是
public static void draw(Shape shape) {}
{% note danger %}
泛型类型只有 extends,没有像通配符那样的 ? super ...
{% endnote %}
类型擦除的影响和桥接方法
有时候,类型擦除会导致一种你可能没有预料到的情况。
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
以下代码
MyNode mn = new MyNode(5);
Node n = mn; // 原始类型 - 编译器会抛出一个未检查警告
n.setData("Hello"); // 导致抛出 ClassCastException。
Integer x = mn.data;
类型擦除后,这段代码变成
MyNode mn = new MyNode(5);
Node n = mn; // 原始类型 - 编译器会抛出一个未检查警告
// 注意:这条语句也可以写成下面这样:
// Node n = (Node)mn;
// 但是,编译器不会生成强制转换,因为它不是必需的。
n.setData("Hello"); // 导致抛出 ClassCastException。
Integer x = (Integer)mn.data;
下面解释为什么在 n.setData("Hello"); 语句上会抛出 ClassCastException
桥接方法
当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要在类型擦除过程中创建一个合成方法,这个方法称为桥接方法。
类型擦除后,Node 和 MyNode 类变成
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
类型擦除之后,方法签名不匹配;Node.setData(T) 方法变成了 Node.setData(Object)。因此,MyNode.setData(Integer) 方法并没有重写 Node.setData(Object) 方法。
那所以在实际上调用的时候,n 的编译期类型是原始类型 Node,所以编译器看到的是 Node.setData(Object data),所以 "Hello" 是能传进去的。
但是在运行期的时候,n 实际指向 MyNode,n.setData("Hello") 根据运行时多态,会去 MyNode 里找 setData(Object),但是发现没有,就会去父类里面找 Node.setData(Object),那就破换了我们本来希望的多态,因为我们 MyNode 明明重写了 Node<Integer> 的 setData(Integer),但是擦除后没有了。
{% note danger %}
但是 mn.setData("Hello") 直接拒绝,因为 mn 是 MyNode 类型,MyNode 只有 setData(Integer),编译期就报错了。
{% endnote %}
为了解决这个问题,并在类型擦除之后保持泛型类型的多态性,Java 编译器会生成一个桥接方法,以确保子类型关系能够按预期工作。MyNode 类,会为 setData 生成下面这种桥接方法
class MyNode extends Node {
// 编译器生成的桥接方法
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
桥接方法 MyNode.setData(Object) 会委托给原来的 MyNode.setData(Integer) 方法。因此,n.setData("Hello"); 语句会调用方法 MyNode.setData(Object),并且由于 “Hello” 不能被强制转换为 Integer,所以会抛出 ClassCastException。
那我可以写代码调用 MyNodeInstance.setData(Object) 吗?
不能,因为源码层面只有 setData(Integer data),编译器生成的桥接方法还没出现。但是反射是在运行期间的,所以可以看到并调用 setData(Object data) 方法,不过注意生成的桥接方法是在里面调用 setData(Integer data),小心类型转换出错。
可以自己在 MyNode 里面自己定义 public void setData(Object data) 方法吗?
其实也不可以了,因为类型擦除会生成一个,和你写的会冲突,所以编译器直接报错 both methods have same erasure, yet neither overrides the other
由此也可以引出一个,那能不能同时继承两个接口?比如我们不是 Node,是 Node<T>,可不可以
class MyNode implements Node<String>, Node<Integer> {}
不可以!!!因为这样每个都会生成一个 setData(Object data),又冲突了,实际上编译器就不知道我这个 setData(Object) 到底该调用 setData(String) 还是 setData(Integer) 了。所以不允许实现不同泛型参数的同一个接口。
泛型的限制
为了有效地使用 Java 泛型,必须考虑以下限制:
- 不能用基本类型实例化泛型类型
- 不能创建类型参数的实例
- 不能声明类型为类型参数的静态字段
- 不能对参数化类型使用强制类型转换或 instanceof
- 不能创建参数化类型的数组
- 不能创建、捕获或抛出参数化类型的对象
- 不能重载那些形式参数类型在擦除后具有相同原始类型的方法
不能用基本类型实例化泛型类型
因为泛型擦除本身就会擦为 Object,基本类型它不是对象,所以不能用
不能创建类型参数的实例
public static <E> void append(List<E> list) {
E elem = new E(); // 编译期错误
list.add(elem);
}
可以通过反射创建类型参数的对象
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
有了上面这个方法,我们可以用下面这种方式调用 append 方法
List<String> ls = new ArrayList<>();
append(ls, String.class);
不能声明类型为类型参数的静态字段
public class MobileDevice<T> {
private static T os;
}
类的静态字段是类级别变量,会被该类的所有非静态对象共享。因此,不允许使用类型参数作为静态字段的类型。
比如,List<Integer> a = ... 和 List<Number> b = ... 都属于 List 对象,他们在运行期间才确定类型,而静态变量在类加载的时候就存在了,先于实例存在,那静态字段的类型根本无法确定。
不能对参数化类型使用强制类型转换或 instanceof
因为 Java 编译器会擦除泛型代码中的所有类型参数,所以无法在运行时验证正在使用的是某个泛型类型的哪一种参数化类型:
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译期错误
// ...
}
}
运行时不会跟踪类型参数,因此它无法区分 ArrayList<Integer> 和 ArrayList<String>。你最多能做的是使用无界通配符来验证这个列表是不是一个 ArrayList:
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK;instanceof 要求可具体化类型
// ...
}
}
通常也不能强制转换为参数化类型,除非它是由无界通配符参数化的。
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译期错误
Object obj = new ArrayList<String>();
List<?> list = (List<?>) obj; // OK,因为 JVM 只检查 obj 是不是 List,不关心具体类型
// 同理上面的 list instanceof ArrayList<?> 也是这样
因为 List<Integer> 并不是 List<Number> 的子类
但是下面这个可以
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>) l1; // OK
List 向下强转为具体实现类型 ArrayList
不能创建参数化类型的数组
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译期错误
因为既然是数组,那我就可以用 Object[] 接收
Object[] stringLists = new List<String>[2]; // 编译器错误,但假设它被允许
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // 应该抛出 ArrayStoreException,
// 但运行时无法检测到它。
如果允许创建,上面这个代码就是对的了!!因为编译期这段代码可以绕过去,运行期又因为泛型擦除,检查不出 List<String> 和 List<String> 的区别。
不能创建、捕获或抛出参数化类型的对象
泛型类不能直接或间接继承 Throwable 类。
// 间接继承 Throwable
class MathException<T> extends Exception { /* ... */ } // 编译期错误
// 直接继承 Throwable
class QueueFullException<T> extends Throwable { /* ... */ // 编译期错误
因为如果允许,那运行期根本分不清 MathException<String> 和 MathException<Integer>,因为擦除后都是 MathException
方法不能捕获类型参数的实例
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // 编译期错误
// ...
}
}
因为 catch 需要在运行期判断异常类型,T 被擦成 Exception,那 JVM 根本不知道你真正想捕获的是 IOException 还是 SQLException
但是可以在 throws 子句写 T
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK:声明方法可能抛出 T
// ...
}
}
不能重载那些形式参数类型在擦除后具有相同原始类型的方法
一个类不能拥有两个重载方法,如果它们在类型擦除后会具有相同的方法签名。
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
这些重载方法在 class 文件中会共享相同的表示形式,并会产生编译期错误。
更多推荐




所有评论(0)