<? extentds T> 和 <? super T> 是一个不容易理解的知识点,网上很多文章都是概念和内容的堆砌,看完之后反而让人更加疑惑。本文不是一蹴而就的,而是源于作者不断的思考和沉淀,力求通过简单的例子,来讲清楚这个知识点。
一个模型
为了接下来的举例说明,先构造 3 个类 Food
、 Fruit
、 Apple
,类之间的继承关系如下图所示:
正确理解 <? extends T> 与 <? super T>
对于 <? extends T> 与 <? super T> 一般解释为:
- <? extends T> 表示对象要么是 T 类型,要么是 T 的子类;
- <? super T> 表示对象要么是 T 类型,要么是 T 的父类。
我们可以通过集合来验证刚刚的解释,示例代码如下所示:
List<Food> foods = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
List<? extends Fruit> assignFoodsToExtendsFruit = foods; // 编译报错
List<? extends Fruit> assignFruitsToExtendsFruit = fruits;
List<? extends Fruit> assignApplesToExtendsFruit = fruits;
List<? super Fruit> assignFoodsToSuperFruit = foods;
List<? super Fruit> assignFruitsToSuperFruit = fruits;
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>
类型的变量,那我们可以在方法体中,对该变量进行添加元素或获取元素操作吗 ?
public static void method(List<? extends Fruit> list) {...}
首先,虽然变量 list
被 List<? extends Fruit>
修饰,但它并不能确定方法被调用时,传入的是对象是 List<Fruit>
还是 List<Apple>
, <? extends Fruit>
仅限制了传入的 list 对象所包含的元素必须是 Fruit
类或其子类。
那可以对变量 list
进行添加元素操作吗? 答案是不可以( null
可以)。
List<Fruit> fruits = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
method(fruits); // 传参合法
method(apples); // 传参不合法
public static void method(List<? extends Fruit> list) {
list.add(new Fruit()); // 如果传入的是 apples,但添加的是 new Fruit,则非法
list.add(null); // 特例,添加 null 是可以的
}
那可以对变量 list
进行获取元素操作吗? 答案是可以。
public static void method(List<? extends Fruit> list) {
Fruit fruit = (Fruit)list.get(0); // 该语句在任何情况下都是合法的
}
因为 <? extends Fruit>
的限制,所以对 list
内的任何元素使用如上的强制转换都是合法的。
- <? super Fruit>
我们将上述例子中方法的入参类型改为 <? super Fruit>
,接着讨论是否可以在方法体内对变量 list
进行添加元素或获取元素操作。
public void method(List<? super Fruit> list) {...}
首先讨论是否可以对 list
进行添加元素,答案是可以。
List<Food> foods = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
method(foods); // 传参合法
method(fruits); // 传参合法
public void method(List<? super Fruit> list) {
list.add((Fruit) new Fruit()); // 不论传入的是foods还是fruits,该语句都合法
list.add((Fruit) new Apple()); // 不论传入的是foods还是fruits,该语句都合法
}
需要注意的是,对于 List<? super Fruit>
,只能添加 **Fruit**
类或其子类。
那可以对 list
进行获取元素吗?答案是不可以。
public void method(List<? super Fruit> list) {
Fruit fruit = (Fruit)list.get(0); // 父类强制转换为子类是非法的
Object object = (Object)list.get(0); // 特例,因为 Object 是超类,可强制转换
}
最后我们对上面的分析结论进行总结:
泛型 | 添加操作 | 获取操作 |
---|---|---|
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。