在前面说过,泛型是不变的,为了实现泛型的协变与逆变,我们可以使用泛型通配符。
通配符
1 | public void test(List<Object> c) { |
这个方法声明没有任何问题,但是调用该方法实际传入的参数值,可能会出错。
考虑如下代码:
1 | List<String> strList = new ArrayList<>(); |
为了表示各种泛型 List 的父类,可以使用类型通配符。List<?>
表示元素类型未知的 List。这个 ?
号被称为通配符,它可以匹配任何类型。
将上面的代码,改为如下形式:
1 | public void test(List<?> c) { |
现在传入任何类型的 List,程序可以正常打印集合 c 中的元素。
这种方法同时带来了另一个问题,即集合中元素的类型会被当成 Object 类型对待。
泛型通配符的上界
1 | class Fruit {} |
现在 flist 的类型是 <? extends Fruit>
,extends 指出了泛型的上界为 Fruit。下边界是任意 Fruit 的子类,理论上来说可以是无限多。使用通配符可以将 ArrayList<Apple>
向上转型了,也就实现了协变。
这样的转换也有一定的副作用。那就是容器的部分功能可能失效。我们不能向一个协变泛型的结构中加入任何元素(除了 null)。观察上面代码,再也不能往容器里任何东西。
因为泛型的类型擦除原因,类型检查移到了编译期,但协变过程又丢掉了具体的类型(可以是 Fruit 类的任何子类,无法确定到底是哪个子类),导致编译器无法确定真实的类型信息,所以拒绝了插入操作。
另一方面,我们知道,不论它是什么类型,它总是 Fruit 的子类型,当我们在读取数据时,能确保得到的数据是一个 Fruit 类型的实例:
1 | Fruit get = flist.get(0); |
总结:
如果一个容器是只读的,才能协变。不然很容易就能把一些特殊的容器协变到更一般的容器,再往里面添加进不应该储存的类型。
协变结构可读,不可写。
泛型通配符的下界
使用通配符 ? super T
,其中 T 是一个基类型,或者说父类,我们可以向逆变结构中添加任何 T 及 T 的子类。
逆变结构可写,不可读。
1 | class Fruit {} |
List<? super Apple>
指定了泛型的下界是 Apple。上边界是模糊的,任意 Apple 的父类都可以,存在无限的可能性,无法确定具体的类型(可以是 Apple 的任何父类,无法确定到底是哪个父类)。所以只能添加 Apple 及其子类。
只能取出 Object 实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个 Object,因为 Object 是任何 Java 类型的超类。
存取原则
- 如果你想从一个数据类型里获取数据,使用
<? extends T>
通配符。 - 如果你想把对象写入一个数据结构里,使用
<? super T>
通配符。 - 如果你既想存,又想取,那就别用通配符。