一段编译不过的代码
上周写了个把 List<Integer> 拷到 List<Number> 的工具方法,编译直接红了。我当时第一反应是:Integer 不是 Number 的子类吗,这也不行?
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> nums = ints; // 编译报错:incompatible types
报错信息是 Required: List<java.lang.Number> Found: List<java.lang.Integer>。带我的师傅瞄了一眼说:"你这是没搞明白泛型不协变,也该看看 PECS 了。"
为什么 List<Integer> 不是 List<Number>
如果上面那行能编译过,那就会出现这种灾难:
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // 假设合法
nums.add(3.14); // 往里塞了个 Double
Integer i = ints.get(0); // ClassCastException
一个装 Integer 的 List,被强转成 Number 视图后塞进 Double,取出来的时候炸了。Java 为了让这种错误在编译期就拦住,直接规定 泛型类型不具备协变性:不管 B 是不是 A 的子类,List<B> 和 List<A> 就是两个没有继承关系的类型。
那确实需要"这一族类型都能传进来"怎么办?这就是通配符 ? 存在的意义。
? extends:只读不写
List<? extends Number> list = new ArrayList<Integer>();
Number n = list.get(0); // OK,读出来一定是 Number
list.add(1); // 编译报错
读没问题,写就一定报错,哪怕你写的是 Integer。原因不难想:编译器只知道这个 List 装的是"某个 Number 的子类",但不知道具体是 Integer 还是 Double 还是 BigDecimal,写任何一个具体类型都不安全。所以 ? extends T 的语义是"我是 T 的某种子类型的集合",编译器索性禁止写入(除了 null)。
? super:只写不读(读到的是 Object)
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // OK,Integer 一定是"Integer 的某个父类"的子类
Integer i = list.get(0); // 编译报错,只能拿到 Object
Object o = list.get(0); // OK
写是安全的,因为不管实际类型 Integer / Number / Object,塞个 Integer 进去都成立。读就不行了,编译器没法确定拿出来的是什么,只能给个 Object。
PECS 原则
Effective Java 里总结成一句话:Producer Extends, Consumer Super。
- 如果这个容器你是从里面往外拿数据的(它是生产者),用
? extends T。 - 如果这个容器你是往里面塞数据的(它是消费者),用
? super T。 - 又读又写,别用通配符,直接写
List<T>。
JDK 自己的源码就是最好的例子,Collections.copy:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
src 只被读,是生产者,? extends T;dest 只被写,是消费者,? super T。这样 copy(List<Number>, List<Integer>) 和 copy(List<Object>, List<Long>) 都能过。我照着这个签名改了自己的工具方法,调用方终于不用到处做强转了。
最后补一个容易混的点:List<?> 和 List<Object> 不是一回事。List<Object> 表示"这个 List 装的就是 Object",你可以往里 add 任何对象;List<?> 表示"装的是某种未知类型",只能读不能写。List<?> 是所有 List<X> 的父类型,如果你要写一个能接收任意 List 又不需要修改它的方法,参数就该用 List<?>。
类型擦除带来的限制
这里还有个我一直没想明白的问题:为什么不能 new T()、不能 instanceof List<String>、不能重载 void f(List<String>) 和 void f(List<Integer>)?
因为 Java 的泛型是编译期语法糖,编译完泛型信息就被擦掉了。用 javap 看一眼:
$ javap -c -p MyClass.class
public void add(java.lang.Object);
// 泛型 T 擦除成了 Object
擦除规则:无界 T 和 ? 擦成 Object,? extends Number 擦成 Number,? super Integer 擦成 Object。运行时 JVM 眼里只有原始类型 List,所以 list instanceof List<String> 这种判断根本没法做——它和 list instanceof List<Integer> 擦完是同一句,编译器直接给你报错 illegal generic type for instanceof。
同理 new T() 也不行,因为运行时不知道 T 是什么。真需要的话得传 Class 对象进来:
public static <T> T create(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
super 的一个真实例子
另外一个我实际用到 super 的地方是往集合里批量添加元素。写个工具方法把数组元素塞进集合,用 super 就能让这个方法同时支持 List<Long> 和 List<Object>,调用方灵活很多。
extends 的例子好找,super 的我一开始想不出什么场景,直到写了个求最大值的方法:
public static <T> T max(List<? extends T> list, Comparator<? super T> c) {
T max = list.get(0);
for (T t : list) {
if (c.compare(t, max) > 0) {
max = t;
}
}
return max;
}
Comparator<? super T> 在这里的好处很实在:如果我有一组 Student 对象,而手上只有一个 Comparator<Person>(按年龄排),用 super 就能直接复用,不用专门为 Student 再写一遍比较器。JDK 里的 Collections.sort(List<T>, Comparator<? super T>) 和 TreeMap 的构造方法都是这个签名。
顺带说下 Collections.sort 在 JDK 8 里也带了个无参重载 sort(List<T>),它要求 T 实现 Comparable。用起来方便,但如果你的类没实现 Comparable,编译期就会报 no suitable method found,这时候别去改 List 的泛型,去给类加 Comparable 或者传比较器。
小结
我给自己记的口诀就两条:往外拿用 extends,往里塞用 super。记不住的时候就想 Collections.copy 那个签名。至于擦除,记住"泛型只活在编译期"这一句,大部分诡异报错都能解释通。