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. 常见误区
- 以为
List<Object>能收List<String>——泛型不传递,<Object>只是一个具体类型;能通吃的只有List<?>。 - 往
? extends容器里 add——即使塞"看起来兼容"的类型也报错;extends 门只读(null 除外)。 - 从
? super容器读出来当具体类型——只能当 Object 接;要具体类型得强转(那就失去泛型意义了)。 - 把
?写在类或方法声明上——class Box<?>✗;?只写在使用的类型里(变量、参数、返回值)。 - 以为
?等于<T>的简写——<T>是"占位符,等我定型",?是"未知,永远不定型";泛型方法要读写操作数据用<T>,只要兼容传参用?。 - 三扇门乱选——先问这个参数"我只读还是只写":只读 extends、只写 super、都要动就别用
?(用<T>或具体类型)。
9. 折叠自测题
List<Object> 是一个具体类型(只能装 Object 的列表);List<?> 是"某种未知类型的列表"的统称——List<String>、List<Integer> 都能赋给它,List<Object> 反而不能收 List<String>。一句话:<?> 是万能插座,
? extends Number 可能实际是 List<Double>、List<Float>……编译器不知道具体是谁,无法保证塞 Integer 合法。为了不破坏传入列表的类型安全,干脆禁止一切写入(null 除外)。
能传 List<Integer>、List<Number>、List<Object>(Integer 及其祖先);能写入 Integer 及其子类;读出来只能当 Object。口诀:下界管写,读出来都是 Object。
Producer Extends,Consumer Super:参数是生产者(只往外提供数据,如 sum 的入参)就用 ? extends;参数是消费者(只往里接收数据,如 fill 的入参)就用 ? super。Collections.copy 的签名是教科书示范。
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接口:集合泛型签名的主场,本节补完最后一块