<? extentds T> 和 <? super T> 是一个不容易理解的知识点,网上很多文章都是概念和内容的堆砌,看完之后反而让人更加疑惑。本文不是一蹴而就的,而是源于作者不断的思考和沉淀,力求通过简单的例子,来讲清楚这个知识点。

一个模型

为了接下来的举例说明,先构造 3 个类 FoodFruitApple ,类之间的继承关系如下图所示: <? extends T> 和 <? super T> - 图1

正确理解 <? extends T> 与 <? super T>

对于 <? extends T> 与 <? super T> 一般解释为:

  • <? extends T> 表示对象要么是 T 类型,要么是 T 的子类;
  • <? super T> 表示对象要么是 T 类型,要么是 T 的父类。

我们可以通过集合来验证刚刚的解释,示例代码如下所示:

  1. List<Food> foods = new ArrayList<>();
  2. List<Fruit> fruits = new ArrayList<>();
  3. List<Apple> apples = new ArrayList<>();
  4. List<? extends Fruit> assignFoodsToExtendsFruit = foods; // 编译报错
  5. List<? extends Fruit> assignFruitsToExtendsFruit = fruits;
  6. List<? extends Fruit> assignApplesToExtendsFruit = fruits;
  7. List<? super Fruit> assignFoodsToSuperFruit = foods;
  8. List<? super Fruit> assignFruitsToSuperFruit = fruits;
  9. List<? super Fruit> assignApplesToSuperFruit = apples; // 编译报错

上面的代码中,共有两处编译报错,的确验证了我们之前对 <? extends T> 和 <? super T> 的说明。但除此之外,我们往往忽略了更重要的一点: List<? extends Fruit>List<? super Fruit> 修饰的仅仅是一个集合引用,而该引用指向的其实是一个已经指明了元素类型的集合。

这也符合我们的日常认知:在 Java 标准库中,类似 <? extends T> 和 <? super T> 的定义往往出现在类或接口的定义、方法入参类型的定义、方法返回值的定义,而从未用于构造一个对象。

只出不进 or 只进不出

  • <? extends Fruit>

现假设有如下方法,方法的入参是一个 List<? extends Fruit> 类型的变量,那我们可以在方法体中,对该变量进行添加元素或获取元素操作吗 ?

  1. public static void method(List<? extends Fruit> list) {...}

首先,虽然变量 listList<? extends Fruit> 修饰,但它并不能确定方法被调用时,传入的是对象是 List<Fruit> 还是 List<Apple><? extends Fruit> 仅限制了传入的 list 对象所包含的元素必须是 Fruit 类或其子类。

那可以对变量 list 进行添加元素操作吗? 答案是不可以null 可以)。

  1. List<Fruit> fruits = new ArrayList<>();
  2. List<Apple> apples = new ArrayList<>();
  3. method(fruits); // 传参合法
  4. method(apples); // 传参不合法
  5. public static void method(List<? extends Fruit> list) {
  6. list.add(new Fruit()); // 如果传入的是 apples,但添加的是 new Fruit,则非法
  7. list.add(null); // 特例,添加 null 是可以的
  8. }

那可以对变量 list 进行获取元素操作吗? 答案是可以

  1. public static void method(List<? extends Fruit> list) {
  2. Fruit fruit = (Fruit)list.get(0); // 该语句在任何情况下都是合法的
  3. }

因为 <? extends Fruit> 的限制,所以对 list 内的任何元素使用如上的强制转换都是合法的。

  • <? super Fruit>

我们将上述例子中方法的入参类型改为 <? super Fruit> ,接着讨论是否可以在方法体内对变量 list 进行添加元素或获取元素操作。

  1. public void method(List<? super Fruit> list) {...}

首先讨论是否可以对 list 进行添加元素,答案是可以

  1. List<Food> foods = new ArrayList<>();
  2. List<Fruit> fruits = new ArrayList<>();
  3. method(foods); // 传参合法
  4. method(fruits); // 传参合法
  5. public void method(List<? super Fruit> list) {
  6. list.add((Fruit) new Fruit()); // 不论传入的是foods还是fruits,该语句都合法
  7. list.add((Fruit) new Apple()); // 不论传入的是foods还是fruits,该语句都合法
  8. }

需要注意的是,对于 List<? super Fruit>只能添加 **Fruit** 类或其子类

那可以对 list 进行获取元素吗?答案是不可以

  1. public void method(List<? super Fruit> list) {
  2. Fruit fruit = (Fruit)list.get(0); // 父类强制转换为子类是非法的
  3. Object object = (Object)list.get(0); // 特例,因为 Object 是超类,可强制转换
  4. }

最后我们对上面的分析结论进行总结:

泛型 添加操作 获取操作
List<? extends Fruit> 非法, add(null) 除外 合法,默认为 Fruit 类型
List<? super Fruit> 合法,仅可添加 Fruit 或其子类 非法,但可强转换为 Object 对象

总结到一句话就是:**<? extends T>** 只出不进,**<? super T>** 只进不出

建议自己再独自推理一遍,如果还是混淆结论,建议重读“正确理解 <? extends T> 与 <? super T>”小节。

如何选择 <? extends T> 和 <? super T>

如果你已经理解了上面的全部内容,那对于如何选择 <? extends T><? super T> ,你只需要记住以下两点:

  • 不论是 **<? extends T>** 还是 **<? super T>** ,都是以类型 **T** 为中心的;
  • 如果你想使用 **T** 类对象,则用 **<? extends T>** ;如果你想添加 **T** 类对象,则用 **<? super T>**

《Effective Java》对第 2 点提出了一个 PECS 原则,即 Producer-extends, Consumer-super。