泛型通配符与上下限 | JavaSE
泛型通配符与上下限
一、学习目标
完成本章后,你应该能够:
- 能够解释泛型通配符
?表示什么。 - 能够区分类型参数
T与通配符?。 - 能够解释为什么
List<Dog>不是List<Animal>的子类型。 - 能够使用无界通配符
<?>。 - 能够正确理解
? extends T的上限通配符。 - 能够正确理解
? super T的下限通配符。 - 能够判断上限通配符集合中哪些读取操作是安全的。
- 能够判断下限通配符集合中哪些写入操作是安全的。
- 能够理解 PECS:Producer Extends,Consumer Super。
- 能够区分“有界类型参数
<T extends ...>”与“有界通配符? extends ...”。 - 能够为集合 API 选择合理的泛型边界。
二、核心知识
2.1 为什么需要通配符
假设存在:
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
普通对象之间存在:
Dog dog = new Dog();
Animal animal = dog;
因为:
Dog
↓
Animal
所以:
Dog 是 Animal 的子类型
自然允许:
Dog 对象
→ Animal 引用
很多初学者于是会进一步认为:
既然 Dog extends Animal
那么 List<Dog>
应该也是 List<Animal> 的子类型
但这是错误的。
2.2 List 不是 List
下面不能成立:
List<Dog> dogs =
new ArrayList<>();
// List<Animal> animals = dogs;
为什么 Java 不允许?
假设允许:
List<Dog> dogs =
new ArrayList<>();
List<Animal> animals = dogs;
因为:
animals 的类型是 List<Animal>
那么下面理论上就合法:
animals.add(new Cat());
可是:
animals
实际指向的对象却是:
List<Dog>
于是:
Dog 集合
↓
塞进 Cat
类型安全彻底被破坏。
因此 Java 必须阻止:
List<Dog>
→
List<Animal>
这种直接赋值。
这叫:
Java 泛型默认具有不变性(Invariant)的典型表现。
简单记:
Dog extends Animal
不代表
List<Dog> extends List<Animal>
2.3 那么如何接收多种相关泛型类型
假设我们设计:
public static void showAnimals(
List<Animal> animals
) {
}
那么可以传:
List<Animal>
但:
List<Dog>
List<Cat>
不能直接传。
如果方法只是想:
接收任意一种 Animal 或 Animal 子类组成的集合并读取它们。
这时就需要:
泛型通配符(Wildcard)
2.4 什么是 ?
泛型中的:
?
称为:
通配符(Wildcard)。
它表示:
某个未知的类型
例如:
List<?> list
可以理解成:
这是一个 List,但我现在不知道里面元素的准确类型。
它可能是:
List<String>
List<Integer>
List<Student>
List<Dog>
...
2.5 ? 与 T 有什么区别
这是本章最重要的区别之一。
定义:
public static <T> void print(
List<T> list
) {
}
这里:
T
是:
被正式声明、具有名字的类型变量
方法内部可以继续使用它:
T value = list.get(0);
而:
public static void print(
List<?> list
) {
}
这里:
?
表示:
某个未知类型
我们不知道它叫什么,也不能写:
// ? value = list.get(0);
所以可以粗略理解:
T
→ 我给这个未知类型起了名字
→ 后续可以继续建立类型关系
?
→ 我只知道这里存在某个类型
→ 但不关心或不知道它到底是谁
2.6 无界通配符 List<?>
例如:
public static void printList(
List<?> list
) {
for (Object value : list) {
System.out.println(value);
}
}
可以调用:
printList(
List.of("Java", "Python")
);
printList(
List.of(10, 20, 30)
);
因为:
List<?>
可以表示各种具体参数化 List。
2.7 List<?> 为什么可以安全读取 Object
虽然不知道:
? 到底是什么类型
但无论它是:
String
Integer
Dog
Student
它一定至少属于:
Object
体系。
所以:
Object value = list.get(0);
是安全的。
但不能直接:
String value = list.get(0);
因为:
? 不一定是 String
2.8 为什么 List<?> 通常不能加入普通对象
假设:
List<?> list
真实对象可能是:
List<String>
也可能是:
List<Integer>
那么你准备:
list.add("Java");
如果它真实类型是:
List<Integer>
就会破坏类型安全。
加入:
100
又可能破坏:
List<String>
因此对于:
List<?>
编译器不知道什么非 null 元素一定安全。
所以不能随意:
list.add(...)
2.9 上限通配符 ? extends T
原课程使用汽车体系:
class Car {
}
class Xiaomi extends Car {
}
class BYD extends Car {
}
class Dog {
}
需求:
一个赛车方法只能接收 Car 或 Car 子类组成的集合。
可以:
public static void go(
List<? extends Car> cars
) {
}
这里:
? extends Car
表示:
未知类型 ? 必须是 Car 或 Car 的某个子类型。
因此可以传:
List<Car>
List<Xiaomi>
List<BYD>
不能传:
List<Dog>
2.10 extends 在这里不是只表示“类继承”
注意:
? extends Car
中的:
extends
表达的是:
类型上界
如果上界是接口,也仍然使用:
extends
而不是:
implements
例如:
<T extends Comparable<T>>
即使:
Comparable
是接口。
所以泛型中的:
extends
应理解成:
类型必须是这个上界本身或满足该上界约束的子类型。
2.11 ? extends 为什么适合读取
例如:
public static void start(
List<? extends Car> cars
) {
for (Car car : cars) {
car.run();
}
}
为什么能安全读成:
Car
?
因为无论真实类型是:
Car
Xiaomi
BYD
它们都一定可以:
向上转型为 Car
所以:
读取
? extends Car
↓
Car
是安全的。
2.12 ? extends 为什么不能安全添加 Car
假设:
List<? extends Car> cars
真实对象可能是:
List<Xiaomi>
如果允许:
cars.add(new BYD());
那就相当于:
往 Xiaomi 集合中
加入 BYD
显然不安全。
甚至:
cars.add(new Car());
也不行。
因为真实集合可能是:
List<Xiaomi>
所以编译器只知道:
里面一定是某种 Car 子类型
却不知道:
具体是哪一种
因此上限通配符非常适合:
安全读取
却不适合:
向其中写入任意 Car
2.13 下限通配符 ? super T
现在换一个需求:
我需要向一个集合中安全加入 Car。
那么这个集合可以是:
List<Car>
List<Object>
因为:
Car 可以放进 Car 集合
Car 也可以放进 Object 集合
于是:
public static void addCar(
List<? super Car> cars
) {
cars.add(new Car());
}
这里:
? super Car
表示:
未知类型必须是 Car 本身或 Car 的某个父类型。
2.14 ? super Car 能接收什么
例如:
List<Car> cars =
new ArrayList<>();
List<Object> objects =
new ArrayList<>();
addCar(cars);
addCar(objects);
都可以。
如果有:
class Vehicle {
}
class Car extends Vehicle {
}
那么还可能:
List<Car>
List<Vehicle>
List<Object>
都符合:
? super Car
2.15 ? super 为什么适合写入
对于:
List<? super Car> list
真实集合至少能接受:
Car
因为可能是:
List<Car>
List<Vehicle>
List<Object>
无论哪一种:
list.add(new Car());
都安全。
而且 Car 子类同样可以:
list.add(new Xiaomi());
list.add(new BYD());
因为:
Xiaomi → Car
BYD → Car
都可以向上转换。
2.16 ? super 为什么读取时只能稳妥当 Object
考虑:
List<? super Car> list
它可能实际上是:
List<Car>
也可能是:
List<Vehicle>
甚至:
List<Object>
所以:
Object value = list.get(0);
一定安全。
但是:
Car car = list.get(0);
不安全。
因为如果真实对象是:
List<Object>
其中原本可能就保存:
String
Integer
所以从:
? super Car
读取时不能保证得到的一定是 Car。
三、使用方法
3.1 上限通配符:读取 Number
例如:
public static double sum(
List<? extends Number> numbers
) {
double result = 0;
for (Number number : numbers) {
result += number.doubleValue();
}
return result;
}
可以:
List<Integer> ints =
List.of(10, 20, 30);
List<Double> doubles =
List.of(1.5, 2.5);
System.out.println(sum(ints));
System.out.println(sum(doubles));
因为:
Integer extends Number
Double extends Number
因此:
List<Integer>
List<Double>
都能够匹配:
List<? extends Number>
3.2 下限通配符:写入 Integer
例如:
public static void addNumbers(
List<? super Integer> list
) {
list.add(10);
list.add(20);
list.add(30);
}
可以传:
List<Integer>
List<Number>
List<Object>
因为这些集合都能够安全保存:
Integer
3.3 类型参数上限
还有另一种语法:
public static <T extends Number>
double sum(T[] values) {
double result = 0;
for (T value : values) {
result += value.doubleValue();
}
return result;
}
这里不是通配符。
这是:
有界类型参数(Bounded Type Parameter)
含义:
T 必须是 Number 或 Number 子类型
于是:
Integer[]
Double[]
Float[]
可以使用。
3.4 <T extends Number> 与 ? extends Number
二者很像:
<T extends Number>
和:
? extends Number
但用途不同。
<T extends Number>
是在:
定义一个有名字的类型变量 T
例如:
public static <T extends Number>
T first(List<T> list) {
return list.get(0);
}
方法内部可以持续使用:
T
建立:
参数类型
返回类型
局部变量类型
之间的关系。
? extends Number
是:
某个未知的 Number 子类型
例如:
public static void print(
List<? extends Number> list
) {
}
方法并不需要知道:
它究竟是 Integer
还是 Double
只需要知道:
读取出来至少可以看成 Number
3.5 什么时候用 T,什么时候用 ?
一个非常实用的判断方式:
如果多个位置必须表达:
“它们是同一个具体类型”
通常需要:
T
例如:
public static <T> T first(List<T> list)
这里需要建立:
List 中的 T
=
返回值 T
而如果只是:
“我不关心具体类型,只要求满足某个范围”
可以考虑:
?
例如:
public static void print(
List<?> list
)
四、原理与进阶
4.1 PECS
理解上下限最经典的原则是:
PECS
即:
Producer Extends
Consumer Super
可以先把它翻译成:
数据来源 / 生产者
→ extends
数据接收方 / 消费者
→ super
4.2 Producer Extends
例如:
List<? extends Number>
方法主要从集合中:
取 Number
集合是数据来源:
Producer
因此使用:
extends
例如:
for (Number number : list) {
}
很安全。
4.3 Consumer Super
例如:
List<? super Integer>
方法主要向集合中:
写入 Integer
集合负责接收数据:
Consumer
因此使用:
super
例如:
list.add(100);
安全。
4.4 PECS 不是“extends 永远只能读,super 永远只能写”
这是一种便于记忆的总结,不要机械化。
例如:
List<? extends Number>
当然仍然可以:
list.get(0)
list.size()
list.clear()
所谓:
extends 适合读
主要指:
无法安全添加一个普通的 Number 或其任意具体子类型。
而:
List<? super Integer>
也不是完全“不能读”。
可以:
Object value = list.get(0);
只是:
读取出来后无法保证比 Object 更具体的静态类型。
所以 PECS 是:
API 设计原则
不是一句必须机械套用的语法口诀。
4.5 为什么泛型默认不协变
如果:
List<Dog>
可以直接转:
List<Animal>
那么:
List<Animal> animals = dogs;
animals.add(new Cat());
就会污染:
List<Dog>
因此 Java 泛型不允许这种普通参数化类型之间直接协变。
而:
List<? extends Animal>
提供了受约束的:
协变式读取视图
使我们能够安全地:
把 Dog 当 Animal 读
同时禁止危险写入。
4.6 通配符其实代表“未知但受约束的具体类型”
对于:
List<? extends Car>
不要理解为:
这是一个里面可以同时混放
Car、Xiaomi、BYD 的 List
它真正表达的是:
这个 List 有一个确定但当前未知的元素类型,而这个类型一定是 Car 或其子类型。
它可能整体是:
List<Xiaomi>
也可能整体是:
List<BYD>
正因为不知道究竟是哪一个,
所以才不能随意:
add(new Car())
五、实践应用
5.1 赛车游戏
原课程使用赛车案例。
class Car {
public void run() {
System.out.println("running");
}
}
class Xiaomi extends Car {
}
class BYD extends Car {
}
class Dog {
}
游戏启动方法:
public static void go(
List<? extends Car> cars
) {
for (Car car : cars) {
car.run();
}
}
于是:
List<Xiaomi> xiaomiCars =
new ArrayList<>();
List<BYD> bydCars =
new ArrayList<>();
go(xiaomiCars);
go(bydCars);
而:
List<Dog> dogs =
new ArrayList<>();
// go(dogs);
编译器直接拒绝。
5.2 通用数值统计 API
例如:
public static double average(
List<? extends Number> numbers
) {
if (numbers.isEmpty()) {
return 0;
}
double sum = 0;
for (Number number : numbers) {
sum += number.doubleValue();
}
return sum / numbers.size();
}
这个 API 可以同时处理:
List<Integer>
List<Double>
List<Long>
而不需要为每种 Number 子类写一个方法。
5.3 数据复制
假设要复制 Integer:
public static void copyIntegers(
List<? extends Integer> source,
List<? super Integer> target
) {
for (Integer value : source) {
target.add(value);
}
}
这里:
source
→ 提供 Integer
→ extends
target
→ 接收 Integer
→ super
这就是 PECS 思想最直观的组合。
六、常见问题
6.1 ? 是一个类型参数名吗?
不是。
? 表示:
未知类型
而:
T
是一个正式声明、有名字的类型变量。
6.2 为什么 List 不能赋值给 List?
因为这样会允许调用者通过:
List<Animal>
向实际:
List<Dog>
中加入其他 Animal,例如 Cat,从而破坏类型安全。
6.3 ? extends Car 能接收 Car 自己吗?
可以。
它表示:
Car
或者 Car 子类型
所以:
List<Car>
List<Xiaomi>
List<BYD>
都可以。
6.4 ? super Car 能接收 Car 自己吗?
也可以。
表示:
Car
或者 Car 父类型
6.5 为什么 extends 中不能直接 add(new Car())?
因为实际集合可能是:
List<Xiaomi>
加入:
Car
不一定满足真实元素类型要求。
6.6 ? super Car 能添加 Xiaomi 吗?
可以。
如果:
Xiaomi extends Car
那么 Xiaomi 可以向上转型为 Car,
而任何能够安全保存 Car 的集合,自然也能保存 Xiaomi。
6.7 ? super Car 取出来为什么只能可靠当 Object?
因为真实集合可能是:
List<Object>
里面原本可能包含各种 Object。
因此无法保证取出的元素一定是:
Car
6.8 List<?> 等于 List<Object> 吗?
不等于。
List<Object>
表示:
元素类型明确就是 Object
可以向其中加入:
new Object()
"Java"
100
而:
List<?>
表示:
元素的准确类型未知
可能实际是:
List<String>
所以不能随便向里面加入普通对象。
6.9 <T extends Number> 与 ? extends Number 一样吗?
不一样。
<T extends Number>
声明一个可重复引用的:
有名字的类型变量 T
而:
? extends Number
表示一个:
未知但具有 Number 上界的类型
6.10 什么是 PECS?
Producer Extends
Consumer Super
当一个泛型参数主要:
向方法提供数据
考虑:
extends
当它主要:
接收方法写入的数据
考虑:
super
七、练习与验收
7.1 知识问答
- 泛型中的
?表示什么? ?和T有什么区别?- 为什么 Dog 是 Animal 子类,但
List<Dog>不是List<Animal>子类型? - 什么是
List<?>? List<?>可以安全读成什么类型?- 为什么不能随便向
List<?>中加入对象? ? extends Car表示什么?? super Car表示什么?? extends Car为什么适合读取 Car?- 为什么不能安全向
? extends Car加入普通 Car? ? super Car为什么可以安全加入 Car?? super Car为什么读取时只能稳妥看成 Object?- 什么是 PECS?
<T extends Number>与? extends Number有什么区别?List<Object>与List<?>有什么区别?
7.2 代码阅读
设:
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
阅读:
List<Dog> dogs =
new ArrayList<>();
// List<Animal> animals = dogs;
回答:
- 被注释的代码为什么不合法?
- 如果 Java 允许这种赋值,会产生什么类型安全问题?
- 请设计一段“向 dogs 中间接塞入 Cat”的假想代码证明危险性。
阅读:
public static void print(
List<? extends Number> list
) {
Number value = list.get(0);
// list.add(100);
}
回答:
- 为什么可以读成 Number?
- 为什么
add(100)不安全? - 真实 list 可能是什么具体类型?
阅读:
public static void add(
List<? super Integer> list
) {
list.add(100);
Object value = list.get(0);
}
回答:
- 为什么可以加入 Integer?
- 哪些 List 类型可以传进来?
- 为什么读取时只能安全保存成 Object?
7.3 手写代码
任务一:赛车
定义:
Car
Xiaomi extends Car
BYD extends Car
Dog
实现:
go(List<? extends Car> cars)
要求:
List<Xiaomi>可以传入。List<BYD>可以传入。List<Dog>编译失败。- 遍历并调用 Car 的方法。
任务二:Number 求和
实现:
sum(List<? extends Number>)
要求支持:
Integer
Double
Long
集合。
任务三:下限通配符
实现:
addInteger(
List<? super Integer> list
)
向集合中加入多个整数。
分别测试:
List<Integer>
List<Number>
List<Object>
任务四:有界类型参数
设计:
<T extends Number>
double sum(T[] array)
比较它与:
sum(List<? extends Number>)
的异同。
7.4 Debug
下面代码无法编译:
List<Dog> dogs =
new ArrayList<>();
List<Animal> animals = dogs;
要求:
- 说明为什么。
- 不允许简单使用强制类型转换解决。
- 如果目标只是读取所有 Animal 子类型集合,应该如何设计方法参数?
继续 Debug:
public static void addCar(
List<? extends Car> cars
) {
cars.add(new Car());
}
回答:
- 为什么不能添加?
- 真实集合可能是什么?
- 如果需求是“往 Car 或其父类集合中写入 Car”,应该改成什么?
7.5 综合训练
设计一个汽车处理 API。
已有:
Vehicle
↑
Car
├── Xiaomi
└── BYD
要求实现:
printCars(...)- 接收 Car 或其任意子类集合。
- 只负责读取并输出。
addCars(...)- 能向 Car、Vehicle 或 Object 集合中加入 Car。
- 一个泛型方法:
<T extends Car>- 返回数组中的第一个汽车。
- 对每个参数说明为什么使用:
? extends? super<T extends ...>
- 用 PECS 原则解释设计。
7.6 本章验收
- [ ] 能解释
?的含义。 - [ ] 能区分
?与T。 - [ ] 能解释
List<Dog>为什么不是List<Animal>。 - [ ] 能闭卷写出
? extends Car。 - [ ] 能闭卷写出
? super Car。 - [ ] 能准确解释 extends 为什么适合读取。
- [ ] 能准确解释 super 为什么适合写入。
- [ ] 能解释
List<?>与List<Object>的区别。 - [ ] 能解释
<T extends Number>与? extends Number的区别。 - [ ] 能使用 PECS 分析一个泛型 API。
- [ ] 能独立完成 Car/Xiaomi/BYD 通配符综合案例。