☕ 第八章 泛型
📄 一句话概括

8.1 铁规矩第二条"泛型不传递"(Box<Object> 不是 Box<String> 的父类)在这里正式拆迁:? 通配符三扇门——无界 ?(啥都收但只能看)、上界 ? extends(只读)、下界 ? super(只写)——读写权限一收紧,灵活性就回来了。

1. 回顾那堵墙:泛型不传递

8.1 说过:Integer 是 Number 的子类,但 Box<Integer> 不是 Box<Number> 的子类——泛型没有"传递继承"。这带来一个实际难题:

public static void print(Box<Object> box) { }   // 想通吃

Box<String> bs = new Box<>();
print(bs);   // ✗ 编译报错——Box<String> 和 Box<Object> 平级,传不进去

想把"打印任意 Box"写成一个方法,难道每种类型重载一遍?? 通配符就是官方拆墙方案。

2. 无界通配符 ?:未知类型,啥都收

? 读作"某种未知的类型"——写在使用的地方(不是类上、不是方法上):

public static void printAll(Box<?> box) {       // 任意 Box<?> 都能传
    System.out.println(box.get());              // 读:没问题
}

printAll(new Box<String>());    // ✓
printAll(new Box<Integer>());   // ✓  一份代码通吃
⚠️ 头号坑:能看不能摸

Box<?> 里的 ? 是未知类型——编译器不知道它到底是谁: - 读:✓,取出来当 Object 用(万物皆 Object)。 - 写:✗,box.set("波仔") 编译报错——万一这盒子其实是 Box<Integer> 呢?唯一能塞的是 null(null 可以是任何引用类型)。

记住六字诀:能看,不能摸。

3. 上界通配符 ? extends:只要这一家的

? extends Number = "Number 或它的某个子类,具体是谁未知"。能收紧读的类型:

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {       // 读 ✓:保证是 Number,直接用
        total += n.doubleValue();
    }
    return total;
}

sum(new ArrayList<Integer>());    // ✓
sum(new ArrayList<Double>());     // ✓  Number 一家子全收

// 但写照样被锁:
list.add(1);   // ✗ 编译报错——? 可能是 Double,塞 Integer 就乱了

为什么连 add(1) 都不行?List<? extends Number> 可能实际是 List<Double>——往 Double 列表里塞 Integer 当然犯规。编译器干脆一刀切:extends 门只读不写(null 除外)。

4. 下界通配符 ? super:这一家都能装

? super Integer = "Integer 或它的某个祖先,具体是谁未知"。写的权利回来了:

public static void fill(List<? super Integer> list) {
    list.add(1);       // 写 ✓:Integer 塞进 Integer/Number/Object 容器都安全
    list.add(42);
}

fill(new ArrayList<Integer>());   // ✓
fill(new ArrayList<Number>());    // ✓  祖先容器也收
fill(new ArrayList<Object>());    // ✓

// 读出来只能当 Object(不知道容器到底是什么祖先):
Integer x = list.get(0);          // ✗ 编译报错
Object y = list.get(0);           // ✓

逻辑自洽:容器保证"至少能装 Integer",那往里塞 Integer(及子类)永远安全;但取出来时不知道具体是谁,只能按 Object 接。

5. PECS 口诀 + JDK 实例

三种门的读写权限一张表收拢(以 ? super/extends Integer 或 Number 为例):

写法 读 写 一句话
Box<?> ✓ 当 Object ✗ 只 null 能看不能摸
? extends T ✓ 当 T ✗ 生产者:只往外给
? super T △ 只能当 Object ✓ 塞 T 消费者:只往里收

PECS 口诀(会认级):Producer Extends,Consumer Super——参数要往外生产数据就用 extends,要往里消费数据就用 super。

JDK 里的标准示范(7.7 见过 Collections.copy,现在能看懂签名了):

copy(List<? super T> dest, List<? extends T> src)
//      消费者:只往里装        生产者:只往外给
// 权限分配严丝合缝——读写按需收紧,正是通配符的价值

6. 动手时间(Demo05)

import java.util.*;

public class Demo05 {
    // 生产者:只读——Number 一家子的和
    public static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) {
            total += n.doubleValue();
        }
        return total;
    }

    // 消费者:只写——往 Integer 的任意祖先容器里填数
    public static void fill(List<? super Integer> list, int count) {
        for (int i = 1; i <= count; i++) {
            list.add(i);
        }
    }

    // 无界:只看——打印任意 List
    public static void printAll(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }

    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>(List.of(88, 95, 76));
        List<Double> doubles = new ArrayList<>(List.of(1.5, 2.5));

        System.out.println("整数和: " + sum(ints));       // ✓
        System.out.println("小数和: " + sum(doubles));    // ✓ 同一个方法通吃

        List<Number> nums = new ArrayList<>();
        fill(nums, 3);                                    // ✓ 往祖先容器填
        printAll(nums);
    }
}

7. 30 秒实验

实验 1:printAll 参数写 Box<Object> 再传 Box<String>——编译报错(那堵墙);改成 Box<?> 立即通过——亲眼见拆墙。

实验 2:Box<?> box 上调 box.set("波仔")——编译报错;box.set(null) 却能过——能看不能摸的唯一例外。

实验 3:List<? extends Number> 上 add(1)——编译报错;读出来赋给 Number n 通过——只读门生效。

实验 4:List<? super Integer> 上 add(1) 通过;读出来赋给 Integer 报错、赋给 Object 通过——只写门生效。

实验 5:List<Object> lo = new ArrayList<String>();——编译报错(那堵墙还在);List<?> lq = new ArrayList<String>();——通过。<?> 和 <Object> 是两码事。

8. 常见误区

  1. 以为 List<Object> 能收 List<String>——泛型不传递,<Object> 只是一个具体类型;能通吃的只有 List<?>。
  2. 往 ? extends 容器里 add——即使塞"看起来兼容"的类型也报错;extends 门只读(null 除外)。
  3. 从 ? super 容器读出来当具体类型——只能当 Object 接;要具体类型得强转(那就失去泛型意义了)。
  4. 把 ? 写在类或方法声明上——class Box<?> ✗;? 只写在使用的类型里(变量、参数、返回值)。
  5. 以为 ? 等于 <T> 的简写——<T> 是"占位符,等我定型",? 是"未知,永远不定型";泛型方法要读写操作数据用 <T>,只要兼容传参用 ?。
  6. 三扇门乱选——先问这个参数"我只读还是只写":只读 extends、只写 super、都要动就别用 ?(用 <T> 或具体类型)。

9. 折叠自测题

❓ Q1:`List` 和 `List` 有什么区别?

List<Object> 是一个具体类型(只能装 Object 的列表);List<?> 是"某种未知类型的列表"的统称——List<String>、List<Integer> 都能赋给它,List<Object> 反而不能收 List<String>。一句话:<?> 是万能插座, 只是其中一种插头。

❓ Q2:为什么 `List` 连 `add(1)` 都不行?

? extends Number 可能实际是 List<Double>、List<Float>……编译器不知道具体是谁,无法保证塞 Integer 合法。为了不破坏传入列表的类型安全,干脆禁止一切写入(null 除外)。

❓ Q3:`? super Integer` 的列表能存哪些类型?读出来呢?

能传 List<Integer>、List<Number>、List<Object>(Integer 及其祖先);能写入 Integer 及其子类;读出来只能当 Object。口诀:下界管写,读出来都是 Object。

❓ Q4:PECS 是什么意思?

Producer Extends,Consumer Super:参数是生产者(只往外提供数据,如 sum 的入参)就用 ? extends;参数是消费者(只往里接收数据,如 fill 的入参)就用 ? super。Collections.copy 的签名是教科书示范。

❓ Q5:写一个 max 方法,返回数字列表里的最大值(用通配符收紧参数)。
public static double max(List<? extends Number> list) {
    double best = Double.NEGATIVE_INFINITY;
    for (Number n : list) {
        double v = n.doubleValue();
        if (v > best) best = v;
    }
    return best;
}

要点:只读 → extends;循环里用 Number 接(保证是数字);Integer/Double 列表通吃。

10. 下一步学什么

  • ☑ 8.6 本章小结 ✅ —— 8.6 本章小结 点此复习:五节串线 + 速查 + 模板 4 段 + 六大名坑 + 毕业大题通用工具箱。第八章完结 🎉

相关笔记

  • 第八章 泛型/8.1 泛型基础:"泛型不传递"铁规矩——本节拆迁的那堵墙
  • 第八章 泛型/8.4 泛型方法:<T> 与 ? 的分工——占位符 vs 未知
  • 第七章 集合/7.7 常用工具类:Collections.copy 的签名本节看懂
  • 第四章 面向对象(下)/4.1 继承:extends/super 的字面含义出处
  • 第七章 集合/7.2 Collection接口:集合泛型签名的主场,本节补完最后一块