文章目录
  1. 1. 泛型不存在协变
  2. 2. 通配符表示的泛型的下界
  3. 3. 通配符表示的泛型上界
  4. 4. 总结

  本文源起于笔者在阅读《Thinking in Java》泛型章节的时候,遇到的关于泛型边界的时候思考的一个问题,笔者就该问题分别询问了三位硕士小伙伴,大家都没有回答正确,因此笔者觉得有必要对此进行一个辨析。问题本身并不复杂,且听我细细道来。
  首先,假设有四个类存在如下的继承关系:

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

  Fruit类是超类,AppleOrange继承了Fruit,而Jonathan继承了Apple

泛型不存在协变

  首先我们来看这样一段代码:

1
2
3
4
5
6
7
8
9
10
package generics;//: generics/NonCovariantGenerics.java
// {CompileTimeError} (Won't compile)
import java.util.ArrayList;
import java.util.List;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
} ///:~

  从注释上可以看到,这段代码是无法通过编译的。你可能以为在第9行里,用Apple类型的ArrayList来初始化Fruit类型的List是向上转型,然而这并不是向上转型,Apple类型的List不是Fruit类型的List,因为Apple类型的List可以持有Apple对象及其子类对象,而Fruit类型的List将持有Fruit类型对象及其子类对象,包括Apple类型对象,它可持有的范围更大。
  因此不能用Apple类型的List初始化Fruit类型的List,这说明泛型不存在协变类型

通配符表示的泛型的下界

  但你还是想建立某种向上转型的关系,于是你想到了通配符:,你可能会想到像下面这样来使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package generics;//: generics/GenericsAndCovariance.java
import java.util.ArrayList;
import java.util.List;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
} ///:~

  很遗憾,从注释中你可以看到,像上面那样使用extends关键字,虽然可以通过编译,但在为List中添加对象的时候,是无法成功的,不论你添加的是Fruit本身,还是它的子类,甚至是Object,都无法添加成功。你唯一可以做的是从List中读取对象。
  那么为什么不可以往该容器中装对象呢?因为List<? extends Fruit>表示该List持有的是“Fruit类的某个不确定的子类型”,但编译器并不知道该List中持有的究竟是Fruit的哪个子类型,因此无法将诸如AppleFruit的某个具体子类型装入该List,同时Fruit本身也不能被装入,当然,Fruit的父类型Object就更加不可以了,装入这些类时的转型都不是类型安全的。因此,这个通配符表示的泛型下界可以说是无下限的。
  那么为什么可以调用get()方法来读取该List呢?我们已经知道该List中持有的一定是Fruit的某个子类型,因此它的上界是确定的,就是Fruit,因此当取出的时候,将它赋值给一个静态类型为Fruit的对象,就属于向上转型,因此是没有问题的。

通配符表示的泛型上界

  通配符还有一种用法,就是表示泛型的上界,如下面所示:

1
2
3
4
5
6
7
8
9
10
11
package generics;//: generics/SuperTypeWildcards.java
import java.util.List;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
} ///:~

  像上面这样使用List<? super Apple>,就可以向List中添加Apple及其子类Jonathan了,但向其中添加Apple的父类Fruit则是不安全的。原因是List<? super Apple>表示该List持有的是Apple的某个不确定的超类型,既然该List中持有的一定是Apple的某个超类型,那么使用add()方法向其中添加Apple及其子类型,一定是类型安全的;但由于编译器只知道这个List持有的是Apple的某个超类型,但不能确定究竟是哪个超类型,因此向其中添加Fruit类型是不安全的。

总结

  总结一下就是:

  • <? extends T> 表示T的某个不确定的子类,因此不能存放具体的T类对象及其子类,更不能存放T的父类,因此Object类不能存放。
  • <? super T> 表示T的某个不确定的超类,因此可以存放T类对象及T的子类对象。
文章目录
  1. 1. 泛型不存在协变
  2. 2. 通配符表示的泛型的下界
  3. 3. 通配符表示的泛型上界
  4. 4. 总结