下面这些 PECS 原则 相关的面试题,你能回答上来多少?
如果回答不了,本文就绝对值得你认真看完。

泛型通配符的用法有些复杂,Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则 ,帮助理解及正确使用 Java 泛型通配符。
当我们真正理解了 Java 泛型的 PECS 原则 ,上面这些问题也就迎刃而解了。
泛型 学习资料:《泛型最全知识导图》、《大厂泛型面试真题26道》,到本篇结尾处获得~
1 知识回顾
首先,我们来简单温习下泛型通配符的相关基础知识。
Java 的泛型通配符为一个泛型类所指定的类型集合,提供了一个有用的类型范围。
如果将一个对象划分为声明、使用两个部分:
- 泛型 则侧重于类型的声明的代码复用,用于 定义内部数据类型的参数化 ;
- 通配符 则侧重于使用上的代码复用,用于 定义使用的对象类型的参数化 。
Java 的 泛型通配符形式 :
- 固定上边界通配符 : ?extends T ,代表类型变量的范围有限,只能传入某种类型、或者它的子类,适合 频繁往外读取数据 的场景。
- 固定下边界通配符 : ? super T ,代表类型变量的范围有限,只能传入某种类型、或者其父类,适合 频繁插入数据 的场景。
更详细深入的介绍,可以看下这两篇(点击蓝字):
2 Java 泛型的 PECS 原则
温顾了基础,我们接着来介绍 PECS 原则 。
PECS 的英文全称是 Producer Extends , Consumer Super ,即读取时使用 extends ,写入时使用 super 。
也就是说:
- 参数化类型表示一个生产者,就使用 <? extends T> ;
- 参数化类型表示一个消费者,就使用 <? super T> 。
不是很明白也没关系,下面的代码示例可以帮助理解。
2.1 我们先来看一个错误
List<? extends Foo> list1 = new ArrayList<Foo>();
List<? extends Foo> list2 = new ArrayList<Foo>();
/* Won't compile */ list2.add( new Foo() ); //error 1
list1.addAll(list2); //error 2
1) error 1
IntelliJ says:
add(capture<? extends Foo>) in List cannot be applied to add(Foo)
The compiler says:
cannot find symbol
symbol : method addAll(java.util.List<capture#692 of ? extends Foo>)
location: interface java.util.List<capture#128 of ? extends Foo>
2) error 2
IntelliJ gives me
addAll(java.util.Collection<? extends capture<? extends Foo>>) in List cannot be applied to addAll(java.util.List<capture<? extends Foo>>)
Whereas the compiler just says
cannot find symbol
symbol : method addAll(java.util.List<capture#692 of ? extends Foo>)
location: interface java.util.List<capture#128 of ? extends Foo>
list1.addAll(list2);
2.2 为什么会出现上面的错误
下面会一步一步,逐步来推导。

Apple 是 Fruit 的子类,但是, List< Apple> 不是 List< Fruit> 的子类,有没有办法让两者兼容使用呢?
这个时候,我们就可以使用型变和逆变来实现了,主要是 extends 和 super 关键字。
例如:
1)型变
HashMap < T extends String>;
HashMap< ? extends String>;
2)逆变
HashMap< T super String>;
HashMap< ? super String>;
2.3 协变 < ? extends T>
类型的上界是 T,参数化类型可能是 T 、又或者 T 的子类。
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IO Exception {
List<? extends Fruit> fruits = new ArrayList<>();
不能加入任何元素:
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile error
fruits.add(new Apple()); // compile error
集合元素的类型,符合 extends Fruit ,可赋值给变量 fruits :
fruits = new ArrayList<Food>(); // compile error
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile success 注1
fruits.add(new Apple()); // compile error 注2
fruits = new ArrayList<? extends Fruit>(); // 在java中会出现 compile error: 通配符类型无法实例化
Fruit object = fruits.get(0); // compile success
}
}
注 1 和注 2 ,两条语句在 kotlin 中,AS 不报错,可以正常运行。
把 Kotlin 转为 Java ,我们发现 Java 代码没有协变,但是其它错误语句在 AS 中是报错的,把 Java 代码贴到 AS 中,是没有报错的。
得出推论 :
AS 检查优先级更高 ,有报错就无法运行,没有报错,就按照 Java 代码去执行。
这是因为 AS 在检查 Kotlin 时,还不够严谨的原因吗?我们来看看。
1)存入数据
- 编译器会阻止将 Apple 类加入 fruits ,在向 fruits 中添加元素时,编译器会检查类型是否符合要求。因为编译器只知道 fruits 是fruit 某个子类的 List,但并不知道这个子类具体是什么类,只能阻止向其中加入任何子类。为了类型安全,不能往使用了 ? extends 的数据结构中写入任何的值;
- 元素类型为 Fruit 和其子类的集合,都可以成功赋值给变量 fruits ,赋值后,变量 fruits 类型就是具体的类型(不再是协变);
- 通配符类型无法实例化。
2)读取数据
由于编译器知道它是 Fruit 的子类型,因此,我们可以从中读取出 fruit 对象:
Fruit fruit = fruits.get(0);
3)kotlin 的协变 out
从关键字可以看出,只能 读出 数据:
var fruits :MutableList<out Fruit>
2.3 逆变<? super T>
表示类型的下界是 T ,参数化类型可以是 T、 或者 T 的超类:
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IOException {
List<? super Fruit> fruits = new ArrayList<>();
fruit 及其子类,可被看做是 fruit ,从而添加成功。
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile success
fruits.add(new Apple()); // compile success
集合元素的类型,符合 super fruit ,可赋值给变量 fruits ,赋值后 fruits 不再是逆变类型。
fruits = new ArrayList<Food>(); // compile success
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile error
fruits = new ArrayList<? super Fruit>(); // compile error: 通配符类型无法实例化
Fruit object = fruits.get(0); // compile error,
}
}
1)kotlin 的逆变——in
从关键字,也能看出,只能 写入 数据
var fruits :MutableList<out Fruit>
2)存入数据
- 添加 fruit 及其子类元素都可以成功,因为编译器会自动向上转型,fruit 及其子类元素,可以被认为是 fruit 类型被成功添加 。但是,由于编译器并不知道 List 的内容,究竟是 fruit 的哪个超类,因此,不允许加入特定的任何超类型。
- 元素类型 为 fruit 和其超类的集合,都可以成功赋值给变量 fruits ,赋值后,变量 fruits 类型就是具体的类型,而不再是逆变。
- super 通配符类型同样不能实例化。
3)读取数据
Object 是任何 Java类的最终祖先类。因此,编译器在不知道这个超类具体是什么类的情况下,只能返回 Object 对象。
Object fruit = apples.get(0);
4)数组是协变的
在 Java 语言中,数组是协变的。如果 Number 是 Integer 的超类型,那么 Number[] 也是 Integer[] 的超类型。
对数组而言, String [] 可以赋值给 Object[] 。
public class Test {
public static void main(String[] args) {
String[] strArray = new String[3];
Object[] objArray = strArray;
}
}
5)kotlin 的泛型实化
泛型实化 在 Java 中是个不存在的概念,属于 Kotlin 的新特性,它可以借助于关键字 inline ,在运行时保留泛型信息。
6)不使用 inline
fun <T> create(): T = mRetrofit.create(T::class.java)
7)使用 inline
inline fun <reified T> create(): T = mRetrofit.create(T::class.java)
这个方法可以被合法声明,在调用时也非常优雅。
val service = create<NetworkService>()
create() 方法不接收任何对象作为参数,只是传入了一个类型参数,根据传入类型的不同,返回我们需要的对象。
原理分析 :
任何被声明称 inline 的函数,都会把函数体内的所有代码,直接复制到每一个被调用的地方。由于泛型参数值的不同,每一个调用 inline 函数的位置,都会因为泛型参数值的不同而有所不同。
它在编译器时,就能确定具体的类型,这样才能实例化。
3 总结
经过上述分析, Java 泛型的 PECS 原则 总结如下:
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符,即 PE( Producer Extends ) ;
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符,即 CS ( Consumer Super ) ;
- 如果既要存又要取,那么就不要使用任何通配符。
以上,是 Java 泛型的 PECS 原则 的 详细介绍。
我是大全哥,持续更新成体系的 Java 核心技术。
知识成体系 , 学习才高效 ,如果觉得有帮助,请顺手 点赞 支持下,谢谢。
我们下期见~
附泛型学习资料:
1 《泛型知识全景导图》
快速构建泛型知识体系,高清版本原图,几乎 囊括了所有泛型核心知识点 。

泛型知识全景导图
2 《大厂泛型面试真题26道》
精选大厂高频 泛型面试题 ,都是我最新整理的,备面、复习时都可以查看 。

大厂泛型面试题26道

— end —