泛型通配符与上下限 | 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 知识问答

  1. 泛型中的 ? 表示什么?
  2. ?T 有什么区别?
  3. 为什么 Dog 是 Animal 子类,但 List<Dog> 不是 List<Animal> 子类型?
  4. 什么是 List<?>
  5. List<?> 可以安全读成什么类型?
  6. 为什么不能随便向 List<?> 中加入对象?
  7. ? extends Car 表示什么?
  8. ? super Car 表示什么?
  9. ? extends Car 为什么适合读取 Car?
  10. 为什么不能安全向 ? extends Car 加入普通 Car?
  11. ? super Car 为什么可以安全加入 Car?
  12. ? super Car 为什么读取时只能稳妥看成 Object?
  13. 什么是 PECS?
  14. <T extends Number>? extends Number 有什么区别?
  15. List<Object>List<?> 有什么区别?

7.2 代码阅读

设:

class Animal {
}

class Dog extends Animal {
}

class Cat extends Animal {
}

阅读:

List<Dog> dogs =
        new ArrayList<>();

// List<Animal> animals = dogs;

回答:

  1. 被注释的代码为什么不合法?
  2. 如果 Java 允许这种赋值,会产生什么类型安全问题?
  3. 请设计一段“向 dogs 中间接塞入 Cat”的假想代码证明危险性。

阅读:

public static void print(
        List<? extends Number> list
) {
    Number value = list.get(0);

    // list.add(100);
}

回答:

  1. 为什么可以读成 Number?
  2. 为什么 add(100) 不安全?
  3. 真实 list 可能是什么具体类型?

阅读:

public static void add(
        List<? super Integer> list
) {
    list.add(100);

    Object value = list.get(0);
}

回答:

  1. 为什么可以加入 Integer?
  2. 哪些 List 类型可以传进来?
  3. 为什么读取时只能安全保存成 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;

要求:

  1. 说明为什么。
  2. 不允许简单使用强制类型转换解决。
  3. 如果目标只是读取所有 Animal 子类型集合,应该如何设计方法参数?

继续 Debug:

public static void addCar(
        List<? extends Car> cars
) {
    cars.add(new Car());
}

回答:

  1. 为什么不能添加?
  2. 真实集合可能是什么?
  3. 如果需求是“往 Car 或其父类集合中写入 Car”,应该改成什么?

7.5 综合训练

设计一个汽车处理 API。

已有:

Vehicle
  ↑
Car
 ├── Xiaomi
 └── BYD

要求实现:

  1. printCars(...)
    • 接收 Car 或其任意子类集合。
    • 只负责读取并输出。
  2. addCars(...)
    • 能向 Car、Vehicle 或 Object 集合中加入 Car。
  3. 一个泛型方法:
    • <T extends Car>
    • 返回数组中的第一个汽车。
  4. 对每个参数说明为什么使用:
    • ? extends
    • ? super
    • <T extends ...>
  5. 用 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 通配符综合案例。