Java 泛型怎么灵活读写?一篇讲透 T、?、extends 与 super 的设计智慧
目录
2.3 上下界对决:? extends T 生产者 vs ? super T 消费者
协变约束:List 是 List 的子集,但父子泛型不可直接赋值(无泛型协变);
读取隔离:下界通配符读取丢失子类类型,只能向上转为 Object。 底层原理:类型擦除,编译后泛型全部转为 Object,通配符只是编译期语法校验。
3.5 链式组合:泛型 + Comparator 多级筛选排序
4. 避坑指南:TreeSet/TreeMap 泛型隐形 Bug
4.3 T extends Comparable 自限定泛型陷阱
1. 面试真题引入
Java 后端面试,泛型通配符是必考题,从一面基础问到二面架构设计。
面试官标准提问流程:
先问「T 和?有什么区别」,你答完类型参数与未知通配符,立刻追问「extends和 super 分别什么时候用,PECS 原则是什么」;
接着落地集合场景:「TreeSet 构造器为什么是Comparator<? super E>」;
最后抛出业务场景:「写一个通用方法,既能读取手机、电脑子类商品,又能批量新增商品,泛型怎么定义?」
三层问题连环追问,只要混淆读写边界、分不清生产者消费者,直接扣分。泛型不是简单语法糖,是一套类型安全的契约设计,吃透底层规则才能避开线上隐藏 bug。
2. 底层原理解构:类型参数与通配符源码透视
2.1 T/E/K/V:固定类型变量,泛型类的「类型模板」
T、E、K、V 属于声明式类型参数,定义泛型类、接口、方法时提前声明,全程统一使用同一个确定类型。 命名约定:
- T:通用任意类型(Type)
- E:集合元素(Element,List/Set 专用)
- K/V:Map 键值(Key/Value)
// 定义时声明T,整个类中T是固定单一类型
public class GoodsBox<T> {
private T goods;
public void set(T goods) { this.goods = goods; }
public T get() { return goods; }
}
核心特点:
- 编译时锁定唯一类型,写入读取完全类型安全;
- 一个泛型类只能绑定一种 T,不能同时兼容父类、多个子类;
- 可加边界约束:
T extends Goods,限定只能是商品及其子类。
本质:给类预留「类型空位」,实例化时填充唯一类型,如同定制包装盒,只能装一类物品。
2.2 通配符?:外挂的灵活占位符
?是使用阶段的未知通配符,仅用于方法参数、变量接收,不能在定义泛型类时声明,代表「不确定的某一类」。
// 通配符仅用在接收参数,无法固定类型
public static void printAll(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add(xxx); 编译报错,无法写入
}
和 T 核心差异对比:
| 维度 | T(类型参数) | ?(通配符) |
| 声明位置 | 类 / 方法定义处<T> |
仅变量、方法入参 |
| 类型确定性 | 全程固定单一类型 | 未知,仅匹配范围 |
| 读写能力 | 可读可写 | 无界通配符只能读 Object,禁止写入 |
| 使用场景 | 自定义泛型工具类 | 通用工具方法,兼容多子类集合 |
2.3 上下界对决:? extends T 生产者 vs ? super T 消费者
1. 上界通配符 ? extends T 生产者(只读)
代表:T 本身 + T 所有子类,只向外输出数据(生产者 Producer)。 规则:只能安全读取为 T,禁止调用 add 写入。
// 只能读取商品,不能新增
public static void showGoods(List<? extends Goods> list) {
Goods g = list.get(0); // 安全读取父类
// list.add(new Phone()); 编译报错!无法写入
}
比喻:仓库只负责出货,只能拿商品,不能往里放新品。
2. 下界通配符 ? super T 消费者(只写)
代表:T 本身 + T 所有父类,只接收外部数据(消费者 Consumer)。 规则:可以安全写入 T 及其子类,读取只能拿到 Object,无法强转父类。
// 可以写入手机子类商品,读取只能Object
public static void addPhone(List<? super Phone> list) {
list.add(new Phone()); // 合法写入子类
Object obj = list.get(0); // 读取只能Object
// Phone p = list.get(0); 编译报错
}
比喻:快递收件箱,只能往里放手机,拿出来分不清是什么物品。
2.4 PECS 设计思想:读写分离的协变逆变模型
PECS 全称:Producer Extends,Consumer Super,泛型通配符核心设计准则:
- 如果方法只读取集合、不新增元素(生产者)→ 使用
? extends T; - 如果方法只新增元素、不依赖读取后的类型(消费者)→ 使用
? super T; - 既要读又要写,不能使用通配符,改用固定类型参数
<T>。
JDK 源码经典佐证:TreeSet 构造器
public TreeSet(Comparator<? super E> comparator)
Comparator 只负责比较元素(接收 E 做参数,属于消费者),因此下界? super E,兼容 E、E 父类比较器。
2.5 泛型契约:类型安全三条数学约束
使用带边界的泛型必须遵守编译期隐式契约,违反直接报类型不匹配:
-
协变约束:
List<Phone>是List<? extends Goods>的子集,但父子泛型不可直接赋值(无泛型协变); -
写入隔离:上界通配符禁止 add 任意实体,避免类型污染;
-
读取隔离:下界通配符读取丢失子类类型,只能向上转为 Object。 底层原理:类型擦除,编译后泛型全部转为 Object,通配符只是编译期语法校验。
3. 纯手工实战:电商商品库存管理系统
3.1 商品分层模型
父类商品,手机、电脑为子类,模拟电商分层品类:
// 顶层父类
public class Goods implements Comparable<Goods> {
private String name;
private Double price;
private Integer stock;
public Goods(String name, Double price, Integer stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
// 自然排序:库存升序
@Override
public int compareTo(Goods o) {
return Integer.compare(this.stock, o.stock);
}
// getter toString省略
public String getName() { return name; }
public Double getPrice() { return price; }
public Integer getStock() { return stock; }
@Override
public String toString() {
return String.format("%-6s 价格:%.2f 库存:%d", name, price, stock);
}
}
// 子类1:手机
public class Phone extends Goods {
private String cpu;
public Phone(String name, Double price, Integer stock, String cpu) {
super(name, price, stock);
this.cpu = cpu;
}
}
// 子类2:电脑
public class Computer extends Goods {
private String gpu;
public Computer(String name, Double price, Integer stock, String gpu) {
super(name, price, stock);
this.gpu = gpu;
}
}
3.2 测试数据
List<Phone> phoneList = Arrays.asList(
new Phone("iPhone16", 5999.0, 120, "A18"),
new Phone("Mate70", 4999.0, 260, "麒麟9010")
);
List<Computer> pcList = Arrays.asList(
new Computer("拯救者", 7999.0, 80, "RTX4060"),
new Computer("MacBook", 8999.0, 50, "M3")
);
3.3 生产者场景:? extends T 批量读取商品
仅读取打印所有商品,不修改集合,符合 Producer 场景,使用上界通配符:
// 生产者:只读取,使用? extends Goods
public static void showAllGoods(List<? extends Goods> goodsList) {
System.out.println("===== 商品列表 =====");
goodsList.forEach(System.out::println);
}
// 调用,手机、电脑子类集合均可传入
showAllGoods(phoneList);
showAllGoods(pcList);
输出:
===== 商品列表 =====
iPhone16 价格:5999.00 库存:120
Mate70 价格:4999.00 库存:260
===== 商品列表 =====
拯救者 价格:7999.00 库存:80
MacBook 价格:8999.00 库存:50
如果强行goodsList.add(),IDE 直接标红编译失败,从语法杜绝类型错误。
3.4 消费者场景:? super T 批量新增商品
统一往库存容器新增手机,只写入不依赖读取类型,Consumer 场景用下界通配符:
// 消费者:只新增手机,使用? super Phone
public static void batchAddPhone(List<? super Phone> box) {
box.add(new Phone("小米14", 3999.0, 300, "骁龙8Gen3"));
box.add(new Phone("一加12", 4299.0, 210, "骁龙8Gen3"));
}
// 父类集合、Phone集合都能接收
List<Goods> allGoods = new ArrayList<>();
batchAddPhone(allGoods);
batchAddPhone(new ArrayList<Phone>());
3.5 链式组合:泛型 + Comparator 多级筛选排序
结合前文 Comparator 链式排序,用泛型工具筛选高价商品并按价格降序:
// 固定类型参数,既能读又能写
public static <T extends Goods> List<T> filterHighPrice(List<T> list) {
// 链式比较:价格降序,库存升序
Comparator<T> priceSort = Comparator
.comparing(T::getPrice, Comparator.reverseOrder())
.thenComparing(T::getStock);
return list.stream()
.filter(g -> g.getPrice() > 5000)
.sorted(priceSort)
.collect(Collectors.toList());
}
// 调用
List<Phone> highPhone = filterHighPrice(phoneList);
highPhone.forEach(System.out::println);
4. 避坑指南:TreeSet/TreeMap 泛型隐形 Bug
4.1 默认排序泛型边界不匹配
TreeSet 默认使用元素Comparable,若泛型边界写死TreeSet<Phone>,只能存 Phone,无法存入 Computer; 如果需要统一存放所有商品,定义为TreeSet<Goods>,或构造器传入Comparator<? super Goods>。
// 正确:下界兼容所有商品比较器
TreeSet<Goods> goodsSet = new TreeSet<>(Comparator.comparing(Goods::getPrice));
4.2 上下界读写混淆,出现编译红线
高频错误:拿到List<? extends Goods>后调用 add,或用List<? super Phone>读取后强转 Phone。 解决方案:严格遵循 PECS,读写分开;既要读又要写就不用通配符,使用<T>固定类型。
4.3 T extends Comparable<T> 自限定泛型陷阱
自定义泛型容器时,很多人写错边界:
// 错误
public class Box<T extends Comparable> {}
// 正确 自限定,保证T自身具备比较能力
public class Box<T extends Comparable<T>> {}
错误写法会丢失泛型参数,TreeSet 排序时触发未经检查的转换警告,极端情况排序错乱。
5. 面试连环炮 Mock Interview
面试官:先说 T 和?的核心区别? 求职者:T 是定义泛型类 / 方法时声明的固定类型参数,全程类型统一,可读可写;? 是仅在使用时的通配符,代表未知类型,分上下界,只读或只写,无法固定类型。
面试官:那extends和 super 分别对应什么场景,PECS 怎么理解? 求职者:PECS 即生产者用 extends,消费者用 super。
? extends T上界:集合只做读取输出,兼容 T 所有子类,禁止写入;? super T下界:集合只接收写入数据,兼容 T 所有父类,读取只能 Object;- 读写都要操作集合,不能使用通配符,改用
<T>类型参数。
面试官:为什么 TreeSet 构造器的 Comparator 是Comparator<? super E>而不是? extends E? 求职者:Comparator 的 compare 方法接收两个 E 参数,只负责接收元素做对比,属于消费者逻辑。使用下界? super E,可以传入 E、E 父类的比较器,扩展性更强;如果用 extends 只能传子类比较器,父类比较器无法传入,限制过大。
面试官:如果我需要一个方法,既能存入手机、电脑,又能读取后拿到完整商品信息,泛型该怎么写? 求职者:不能用通配符,使用带边界的固定类型参数<T extends Goods>,方法入参 List<T>,此时既能 add 子类商品,读取后也能直接转为 Goods / 子类,兼顾读写。
6. 类比小结
把泛型体系比作快递物流系统,一秒分清所有概念:
- T 固定类型参数 = 专属快递箱,只能装一类货物,可放可取;
- ? 无界通配符 = 临时分拣通道,只能看不能放;
? extends T上界生产者 = 出货分拣台,只能取出商品,不能投放;? super T下界消费者 = 统一收件仓,所有子类都能放,取出分不清品类;- PECS 准则 = 物流分工规则,出货用 extends、收件用 super;
- Comparator<? super E> = 通用验货员,既能验手机也能验电脑,兼容所有商品父类规则。
更多推荐
所有评论(0)