目录

1. 面试真题引入

2. 底层原理解构:类型参数与通配符源码透视

2.1 T/E/K/V:固定类型变量,泛型类的「类型模板」

2.2 通配符?:外挂的灵活占位符

2.3 上下界对决:? extends T 生产者 vs ? super T 消费者

1.  上界通配符 ? extends T 生产者(只读)

2.  下界通配符 ? super T 消费者(只写)

2.4 PECS 设计思想:读写分离的协变逆变模型

2.5 泛型契约:类型安全三条数学约束

协变约束:List 是 List 的子集,但父子泛型不可直接赋值(无泛型协变);

写入隔离:上界通配符禁止 add 任意实体,避免类型污染;

读取隔离:下界通配符读取丢失子类类型,只能向上转为 Object。 底层原理:类型擦除,编译后泛型全部转为 Object,通配符只是编译期语法校验。

3. 纯手工实战:电商商品库存管理系统

3.1 商品分层模型

3.2 测试数据

3.3 生产者场景:? extends T 批量读取商品

3.4 消费者场景:? super T 批量新增商品

3.5 链式组合:泛型 + Comparator 多级筛选排序

4. 避坑指南:TreeSet/TreeMap 泛型隐形 Bug

4.1 默认排序泛型边界不匹配

4.2 上下界读写混淆,出现编译红线

4.3 T extends Comparable 自限定泛型陷阱

5. 面试连环炮 Mock Interview

6. 类比小结


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; }
}

核心特点:

  1. 编译时锁定唯一类型,写入读取完全类型安全;
  2. 一个泛型类只能绑定一种 T,不能同时兼容父类、多个子类;
  3. 可加边界约束: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,泛型通配符核心设计准则:

  1. 如果方法只读取集合、不新增元素(生产者)→ 使用 ? extends T
  2. 如果方法只新增元素、不依赖读取后的类型(消费者)→ 使用 ? super T
  3. 既要读又要写,不能使用通配符,改用固定类型参数<T>

JDK 源码经典佐证:TreeSet 构造器

public TreeSet(Comparator<? super E> comparator)

Comparator 只负责比较元素(接收 E 做参数,属于消费者),因此下界? super E,兼容 E、E 父类比较器。

2.5 泛型契约:类型安全三条数学约束

使用带边界的泛型必须遵守编译期隐式契约,违反直接报类型不匹配:

  1. 协变约束:List<Phone>List<? extends Goods> 的子集,但父子泛型不可直接赋值(无泛型协变);
  2. 写入隔离:上界通配符禁止 add 任意实体,避免类型污染;
  3. 读取隔离:下界通配符读取丢失子类类型,只能向上转为 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。

  1. ? extends T 上界:集合只做读取输出,兼容 T 所有子类,禁止写入;
  2. ? super T 下界:集合只接收写入数据,兼容 T 所有父类,读取只能 Object;
  3. 读写都要操作集合,不能使用通配符,改用<T>类型参数。

面试官:为什么 TreeSet 构造器的 Comparator 是Comparator<? super E>而不是? extends E? 求职者:Comparator 的 compare 方法接收两个 E 参数,只负责接收元素做对比,属于消费者逻辑。使用下界? super E,可以传入 E、E 父类的比较器,扩展性更强;如果用 extends 只能传子类比较器,父类比较器无法传入,限制过大。

面试官:如果我需要一个方法,既能存入手机、电脑,又能读取后拿到完整商品信息,泛型该怎么写? 求职者:不能用通配符,使用带边界的固定类型参数<T extends Goods>,方法入参 List<T>,此时既能 add 子类商品,读取后也能直接转为 Goods / 子类,兼顾读写。

6. 类比小结

把泛型体系比作快递物流系统,一秒分清所有概念:

  1. T 固定类型参数 = 专属快递箱,只能装一类货物,可放可取;
  2. ? 无界通配符 = 临时分拣通道,只能看不能放;
  3. ? extends T 上界生产者 = 出货分拣台,只能取出商品,不能投放;
  4. ? super T 下界消费者 = 统一收件仓,所有子类都能放,取出分不清品类;
  5. PECS 准则 = 物流分工规则,出货用 extends、收件用 super;
  6. Comparator<? super E> = 通用验货员,既能验手机也能验电脑,兼容所有商品父类规则。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐