Java入门-泛型通配符

在前面说过,泛型是不变的,为了实现泛型的协变与逆变,我们可以使用泛型通配符。

通配符

1
2
3
4
5
public void test(List<Object> c) {
for (int i = 0; i < c.size(); i++) {
System.out.prinln(c.get(i));
}
}

这个方法声明没有任何问题,但是调用该方法实际传入的参数值,可能会出错。

考虑如下代码:

1
2
List<String> strList = new ArrayList<>();
test(strList); // 编译出错,因为泛型是不变的, List<String> 并不是 List<Object> 的子类。

为了表示各种泛型 List 的父类,可以使用类型通配符。List<?> 表示元素类型未知的 List。这个 号被称为通配符,它可以匹配任何类型。

将上面的代码,改为如下形式:

1
2
3
4
5
public void test(List<?> c) {
for (int i = 0; i < c.size(); i++) {
System.out.prinln(c.get(i));
}
}

现在传入任何类型的 List,程序可以正常打印集合 c 中的元素。

这种方法同时带来了另一个问题,即集合中元素的类型会被当成 Object 类型对待。

泛型通配符的上界

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Apple()); // 编译错误
flist.add(new Fruit()); // 编译错误
flist.add(new Object()); // 编译错误
}
}

现在 flist 的类型是 <? extends Fruit>,extends 指出了泛型的上界为 Fruit。下边界是任意 Fruit 的子类,理论上来说可以是无限多。使用通配符可以将 ArrayList<Apple> 向上转型了,也就实现了协变。

这样的转换也有一定的副作用。那就是容器的部分功能可能失效。我们不能向一个协变泛型的结构中加入任何元素(除了 null)。观察上面代码,再也不能往容器里任何东西。

因为泛型的类型擦除原因,类型检查移到了编译期,但协变过程又丢掉了具体的类型(可以是 Fruit 类的任何子类,无法确定到底是哪个子类),导致编译器无法确定真实的类型信息,所以拒绝了插入操作。

另一方面,我们知道,不论它是什么类型,它总是 Fruit 的子类型,当我们在读取数据时,能确保得到的数据是一个 Fruit 类型的实例:

1
Fruit get = flist.get(0);

总结:

  • 如果一个容器是只读的,才能协变。不然很容易就能把一些特殊的容器协变到更一般的容器,再往里面添加进不应该储存的类型。

  • 协变结构可读,不可写。

泛型通配符的下界

使用通配符 ? super T,其中 T 是一个基类型,或者说父类,我们可以向逆变结构中添加任何 T 及 T 的子类。

逆变结构可写,不可读。

1
2
3
4
5
6
7
8
9
10
11
12
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); // 编译错误
}
}

List<? super Apple> 指定了泛型的下界是 Apple。上边界是模糊的,任意 Apple 的父类都可以,存在无限的可能性,无法确定具体的类型(可以是 Apple 的任何父类,无法确定到底是哪个父类)。所以只能添加 Apple 及其子类。

只能取出 Object 实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个 Object,因为 Object 是任何 Java 类型的超类。

存取原则

  • 如果你想从一个数据类型里获取数据,使用<? extends T> 通配符。
  • 如果你想把对象写入一个数据结构里,使用<? super T> 通配符。
  • 如果你既想存,又想取,那就别用通配符。