Java · 泛型与类型擦除

泛型只活在编译期 — 擦除后字节码里只有一个 raw Box; PECS 通配符口诀; 桥方法与运行时取类型的三板斧

泛型是编译期的类型检查仪 — 擦除后字节码里只有一个 Box ① 类型擦除: 源码两个参数化类型 → 字节码一个 raw 类 Box<String> String s = box.get() Box<Integer> Integer i = box2.get() 两个不同的"编译期类型" javac (只在编译期) ① 检查所有赋值与传参类型 ② T → 上界; 无上界 → Object ③ 调用点自动插入 checkcast ④ 泛型签名存入 Signature 属性 ⑤ 需要时生成桥方法保多态 字节码 (运行时) 只有一个 raw class Box (没有 Box<String>) field: private Object value Object get() — 返回值就是 Object 调用处: checkcast String ← 编译器插的强转 ArrayList<String>.class == ArrayList<Integer>.class Signature 属性: 反射唯一残存的泛型痕迹 * 运行时取回泛型的三板斧: Class<T> token / 超类型令牌 / getGenericSuperclass 两条类型在编译期汇合成一条 — 运行时无泛型信息, 取出来全靠 checkcast 兜底 ② PECS: Producer Extends · Consumer Super — 通配符的方向感 copy 签名 (JDK Collections 同款) copy(List<? super T> dst, List<? extends T> src) T 在方法体内自由流动, 两端各守规矩 Producer · 生产者 (只读源) List<? extends Number> src Number n = src.get(0) ✓ 读按上界 src.add(Integer.valueOf(1)) ✗ 编译错 不知道确切类型 → 写什么都可能错 Consumer · 消费者 (只写目标) List<? super Integer> dst dst.add(Integer.valueOf(1)) ✓ Object o = dst.get(0) 读只能 Object 保证: 任何超类容器都装得下 Integer T (如 Integer) 的数据流向 → 只从生产者流向消费者 类型参数像水管接头 — 组合时两端必须对上, 对不上是编译错而不是线上事故 f : A → B g : B → C ⇒ f.andThen(g) : A → C Legend 源码泛型 编译期动作 字节码/生产者 边界/消费者

擦除的本质

  • • 字节码里没有 T, 字段与方法都按 Object 走
  • • 调用点 checkcast 是编译器替你写的强转
  • • Signature 属性是反射能看到的唯一痕迹
  • • instanceof List<String> 直接编译错

PECS 方向感

  • • 读数据 → ? extends T (生产者)
  • • 写数据 → ? super T (消费者)
  • • 方向搞反不是 bug, 是编译错 — 这是在帮你
  • • JDK 的 Collections.copy 签名是标准答案

运行时取类型三板斧

  • • Class<T> token: foo(Class<T>) 返回 T
  • • 超类型令牌: 匿名子类冻结完整泛型签名
  • • 桥方法: 反射里多出来的合成方法不是 bug
  • • Jackson TypeReference 就是这套原理

💡 一句话理解

Java 泛型是"编译器的类型检查仪", 不是运行时机制: javac 检查完所有赋值与传参后, 把 T 擦成上界(没写上界就擦成 Object), 再在使用处自动插入 checkcast — 所以 Box<String> 与 Box<Integer> 在字节码里是同一个类。这是与 C++ 模板最大的区别: 模板每个实例化都生成一份代码, Java 只有一份 + 编译期把关。代价是 new T()、T.class、instanceof T 都不可能; 换来的好处是不会有 C++ 那种模板代码膨胀, 且泛型代码与老版本 raw 类库二进制兼容。

通配符是这门检查仪的"方向传感器": Producer Extends, Consumer Super — 参数是数据源(只读)就收 ? extends T, 是数据目的地(只写)就收 ? super T。方向搞反不是运行时 bug 而是编译错, 编译器用最粗暴的方式逼你把"这个参数我是读还是写"想清楚 — 这正是 API 设计里最值钱的信息。

🧠 必知必会 必考 & 必会

类型擦除
编译后 T 被替换为上界(无上界 → Object), 字节码只存在一份 raw 类; 泛型是编译器的检查仪, 不是运行时机制。擦除换来的红利: 无代码膨胀 + 与老版本类库二进制兼容。
Box<String> a = new Box<>();
Box<Integer> b = new Box<>();
a.getClass() == b.getClass();  // → true: 只有一个 raw Box
// 关键: 泛型只在编译期把关, 运行时无参数化类型
擦到上界
<T extends Number> 擦成 Number, 方法体内能直接调 Number 的方法; 无界 <T> 擦成 Object, 只能调 Object 那几个方法 — 想多用就加上界。
class Box<T extends Number> {
    double d() { return v.doubleValue(); } // 擦成 Number, 能调
}
// 关键: 无界 <T> 擦成 Object 只剩那几个方法 — 想多用就加上界
checkcast 插入
编译器在"取出泛型值"的位置自动插入: get() 返回 Object, 字节码里紧跟 checkcast String。等价于编译器替你写了强转, 且检查过才让你写。
Box<String> box = ...;
String s = box.get();
// 字节码: get() 返回 Object, 紧跟 checkcast String
// 关键: 强转是编译器替你写的, 且检查过才让你写
new T() 三不可能
运行时没有 T: new T()、T.class、instanceof T 全部编译错。替代: 传 Class<T> token 反射 newInstance, 或直接传工厂 Supplier<T>。
// new T() / T.class / instanceof T 全部编译错 (运行时没有 T)
static <T> T create(Class<T> type) throws Exception {
    return type.getDeclaredConstructor().newInstance();
}
// 关键: 替代 = Class<T> token, 或直接传工厂 Supplier<T>
桥方法
子类覆写泛型父类方法时, 编译器合成一个擦除签名的 bridge 方法内部转发 — 多态的正确性靠它保证。副作用: 反射 getDeclaredMethods 会多出一个 synthetic 方法, 别当成 bug。
class StrNode extends Node<String> {
    @Override public String data() { ... }  // 覆写为具体类型
}
// 编译器合成: bridge Object data() { return data(); } 保多态
// 关键: getDeclaredMethods 多出的 synthetic 桥方法不是 bug
PECS
Producer Extends Consumer Super: 参数是数据源(只读)用 ? extends T, 是目的地(只写)用 ? super T; JDK Collections.copy 的签名就是标准答案, 照抄不会错。
public static <T> void copy(List<? super T> dst,
                          List<? extends T> src) {
    for (T t : src) dst.add(t);
}
// 关键: 只读源 extends, 只写目标 super — Collections.copy 同款
通配符 ?
<?> 只表示"未知类型": 除 null 外不能写入, 读出来是 Object; 与 ? extends T 的区别是它无界 — 表达"我只遍历不关心元素类型", 如 List<?> 作只读任意 List 的参数。
void dump(List<?> list) {              // 只遍历, 不关心元素类型
    for (Object o : list) log("{}", o);
}
// 关键: 除 null 外不能写入; 读出来是 Object
原始类型
List 不带参数直接用 = 跳过泛型检查: 能编译但带 unchecked 警告, CCE 被推迟到调用方爆。只用于遗留代码过渡, 新代码禁止。
List raw = new ArrayList<String>();  // raw: 关掉泛型检查
raw.add(42);                         // 编译过, 仅 unchecked 警告
String s = (String) raw.get(0);      // → CCE 在调用方爆
数组协变 vs 泛型不变
Object[] 能接 String[](协变, 运行时检查, ArrayStoreException); List<Object> 不是 List<String> 的父类(不变, 编译期检查) — 泛型数组因此被禁止。
Object[] a = new String[1];   // 数组协变: 编译过
a[0] = 42;                      // → ArrayStoreException 运行时爆
// List<Object> l = new ArrayList<String>();  编译错: 泛型不变
堆污染 Heap Pollution
参数化类型的引用指向了不含该类型的对象(常经 varargs 的原始数组混入), CCE 在远处爆发; @SafeVarargs 是作者对"我不会污染"的正式承诺。
static void m(List<String>... lists) {}  // varargs 底层是 raw List[]
Object[] a = lists; a[0] = List.of(1);  // 混入, 编译期不报
String s = lists[0].get(0);           // → 远处 ClassCastException
Class<T> token
运行时类型令牌: <T> T pick(Class<T> type) 让"类型参数"与"运行时类型"对齐; 但 token 只能表达 erasure, List<String>.class 这种参数化 token 不存在。
static <T> T pick(Class<T> type, Object src) {
    return type.cast(src);          // 类型参数对齐运行时类型
}
pick(String.class, "x");    // 返回 String; 参数化 token 不存在
超类型令牌
用匿名子类把泛型"冻结"进字节码 Signature: new TypeReference<List<String>>() {}, 再用 getGenericSuperclass() 取回完整参数化类型 — Jackson/Guava TypeToken 同理。
abstract class TypeRef<T> {
    final Type type = ((ParameterizedType) getClass()
        .getGenericSuperclass()).getActualTypeArguments()[0];
}
new TypeRef<List<String>>() {}.type;  // → List<String>; {} 不可省
自限定泛型
<T extends Builder<T>> 让子类方法返回子类型 this, Builder 继承链上链式调用永不丢类型; Lombok @SuperBuilder 帮你生成这套样板。
abstract class Builder<T extends Builder<T>> {
    @SuppressWarnings("unchecked")
    public T self() { return (T) this; }
}
// 关键: 父类方法返回子类型 this, 链式永不丢类型
类型见证
目标类型缺失时推断失败: this.<String>empty() / Collections.<String>emptyList() 显式写出类型参数救编译器 — 链式调用里最常见。
Collections.<String>emptyList();     // 显式类型参数救推断
Matcher.<Order>nonNull().and(eq(x)); // 链式起点无目标类型时必用
// 关键: 见证只写在"推断失败"的那一环

🏭 生产实战 real world

场景 1 · 通用响应包装 Result<T>: 泛型 API 设计

网关统一响应体, 泛型让调用方拿到的是具体类型而不是 Object — 类型链一旦断裂, 全下游退回强转时代:

// 统一响应: data 的类型沿调用链传播, Feign/接口代理全靠这条类型链
public class Result<T> {
    private final int code; private final String msg; private final T data;
    private Result(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; }
    public static <T> Result<T> ok(T data)            { return new Result<>(0, "ok", data); }
    public static <T> Result<T> fail(int c, String m) { return new Result<>(c, m, null); }
    public T data() { if (code != 0) throw new BizException(code, msg); return data; }
}
Result<User> r = userClient.get(uid);   // 泛型沿代理链自动传播
r.data().getCity();                     // data() 即 User, 无强转; 非 0 自动抛

偷懒返回 Result<Object> 的接口, 就是团队里"类型黑洞"的起点。

场景 2 · 类型安全的异构容器: Class<T> 做 key

请求上下文要装各种类型的属性, Map<String, Object> 取出来全靠强转 — Class token 让 key 自带类型:

// 异构容器: key 本身就是类型令牌, 存取类型对得上编译器就满意
public class Attributes {
    private final Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> type, T value) { map.put(type, type.cast(value)); }
    public <T> T get(Class<T> type)             { return type.cast(map.get(type)); }
}
Attributes ctx = new Attributes();
ctx.put(String.class,  "trace-9527");   // 取出来一定是 String
ctx.put(Integer.class, 42);              // type.cast 双保险: 放错立刻 CCE
String tid = ctx.get(String.class);

场景 3 · 超类型令牌: 运行时拿到 List<User>"不存在的泛型"

Jackson 的 TypeReference 人人会用, 原理是"匿名子类把泛型签名冻结进字节码" — 20 行自己造一个:

// Class<T> 表达不了 List<User>.class (被擦除); 匿名子类携带完整 Signature
List<User> users = json.readValue(body, new TypeReference<List<User>>() {});
// 原理: 匿名类的 getGenericSuperclass() 返回 ParameterizedType, 可还原 List<User>

// 造一个 (Guava TypeToken / Spring ParameterizedTypeReference 同理)
public abstract class TypeRef<T> {
    final Type type = ((ParameterizedType) getClass()
            .getGenericSuperclass()).getActualTypeArguments()[0];
}
TypeRef<Map<String, List<User>>> ref = new TypeRef<>() {};  // {} 不可省: 子类才带签名

线上反序列化成 LinkedHashMap 而不是目标类型, 十有八九是走了擦除后的 raw 通道 — 用令牌补救。

场景 4 · 自限定 Builder: 链式调用不丢类型

继承链上的 Builder, 父类方法返回父类型, 链式到子类方法直接编译错 — 自限定让每层返回"自己":

// <T extends Builder<T>>: T 被"钉"在子类上, 返回值永远是子类型 this
public abstract class Builder<T extends Builder<T>> {
    protected String id;
    @SuppressWarnings("unchecked")
    public T id(String id) { this.id = id; return (T) this; }
}
public final class OrderBuilder extends Builder<OrderBuilder> {
    private BigDecimal amount;
    public OrderBuilder amount(BigDecimal a) { this.amount = a; return this; }
}
// new OrderBuilder().id("A1").amount(TEN).build() — id() 后仍是 OrderBuilder
// 无自限定: id() 返回 Builder, 接 amount() 直接编译错; Lombok @SuperBuilder 同原理

场景 5 · 泛型 DAO 基类: 子类三行接入 CRUD

十个 repository 抄十遍 find/all — 泛型基类收拢公共逻辑, Spring Data JPA 的思路一脉相承:

// 泛型基类收拢 CRUD: 子类只声明实体与主键类型
public abstract class Dao<E, ID> {
    private final Class<E> entityClass;      // 擦除下运行时类型标准解: 构造注入 token
    protected Dao(Class<E> entityClass) { this.entityClass = entityClass; }
    public E find(ID id) { return em.find(entityClass, id); }
    public List<E> all() {
        return em.createQuery("FROM " + entityClass.getSimpleName(), entityClass)
                  .getResultList();           // 泛型让查询结果免强转
    }
}
public class UserDao extends Dao<User, Long> { public UserDao() { super(User.class); } }

场景 6 · Function<T,R> 组合: andThen/compose 拼管道

校验/转换逻辑散在五个 if 里 — 函数组合让每段独立单测, 组装处一眼看全流程:

// andThen 正向接管, compose 反向预置 — 类型像水管接头, 两端必须对上
Function<Order, User>   buyer  = Order::user;         // Order → User
Function<User, String>  city   = User::city;          // User → String
Function<Order, String> toCity = buyer.andThen(city); // Order → User → String

// 中间件链: 每段独立单测, 组装即部署 — 接头对不上编译期就红
Function<Req, Req> pipeline = trim.andThen(validate).andThen(fillDefaults);
Req processed = pipeline.apply(rawReq);

场景 7 · PECS 通用 copy: 一个签名通吃所有容器组合

要写一个能把 List<String> 拷进 List<Object> 的方法, 参数怎么声明 — JDK 的答案:

// 读用 extends, 写用 super — T 只在方法体内流动, 两端各守规矩
public static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T t : src) dst.add(t);       // src 只读(生产 T), dst 只写(消费 T)
}
List<Object> bucket = new ArrayList<>();
List<String> names  = List.of("a", "b");
copy(bucket, names);  // List<Object> 是 String 的 super — 合法
copy(names, names);   // String 也是自己的 super — 惊喜但正确

若参数写成两个 List<T>, 第一行调用直接编译不过 — 这就是 API 灵活性的分水岭。

场景 8 · <T extends Comparable<? super T>>: 全 JDK 最讲究的边界

写通用 max 时边界怎么画, 直接决定哪些类型能用 — 逐词拆开这个教科书签名:

// T 可比较; ? super T 放行"父类实现的 Comparable" — 子类白捡比较能力
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    T best = null;
    for (T t : list)
        if (best == null || t.compareTo(best) > 0) best = t;
    return best;
}
// 写死 <T extends Comparable<T>>: 父类实现比较器的子类直接编译不过
// 参数 List<? extends T> 同理: 让 List<Sub> 也能喂进来

场景 9 · 泛型异常的边界: catch 不能泛型, 函数式走私

线上 lambda 里层层 try/catch 包 IOException, 噪音淹没业务 — 受检异常的边界要绕过去:

// catch (T e) 不可能: 擦除后等同 catch(Exception), 泛型约束是幻觉
// 正解: 函数式包装把受检异常"走私"出去
@FunctionalInterface
interface ThrowingSupplier<T> { T get() throws Throwable; }

static <T> T unchecked(ThrowingSupplier<T> sup) {
    try { return sup.get(); }
    catch (Throwable e) { throw new RuntimeException(e); }  // 保留完整原始栈
}
User u = unchecked(() -> json.readValue(body, User.class));  // 一行搞定

场景 10 · default + 泛型: 零依赖的流式断言 DSL

测试断言库不想引依赖 — 接口 default 方法 + 泛型 static 工厂, 30 行拼出可组合的匹配器:

// 组合子在 default 方法上自由生长, 扩展零侵入
public interface Matcher<T> {
    boolean test(T value);
    default Matcher<T> and(Matcher<T> other) {
        return v -> test(v) && other.test(v);    // 组合而非继承
    }
    static <T> Matcher<T> eq(T target) { return v -> Objects.equals(v, target); }
    static <T> Matcher<T> nonNull()    { return Objects::nonNull; }
}
Matcher<Order> rule = Matcher.<Order>nonNull().and(Matcher.eq(paidOrder));

末行的 <Order> 类型见证: 链式起点没有目标类型, 必须显式帮推断一把。

⚠️ 编码注意与常见坑 pitfalls

坑 1 · instanceof List<String> 编译错 — "illegal generic type for instanceof". 原因: 擦除后运行时根本没有这个类型可判. 正解: 只能 instanceof List (raw, 配 unchecked 警告) 或逐元素判类型。
// 错: if (o instanceof List<String>) {}  → illegal generic type for instanceof
// 对: if (o instanceof List<?> l) {}  再逐元素判类型
坑 2 · 静态成员用类型参数 — static T field / static void f(T t) 编译错. 原因: 类只有一份 raw 字节码, static 属于类不属于某个参数化实例. 正解: 泛型方法自带 <T>, 或把 Class<T> 当参数传。
// 错: class Box<T> { static T v; static void f(T t) {} }  → 编译错
// 对: static <T> void f(T t) 泛型方法自带 <T>; 或传 Class<T> 参数
坑 3 · new T[10] 编译错 — "generic array creation". 原因: 数组协变靠运行时类型检查, 泛型擦除后没有运行时类型可查, JVM 明令禁止. 正解: new ArrayList<T>(), 或反射 Array.newInstance(clazz, n)。
// 错: class Box<T> { T[] a = new T[10]; }  → generic array creation
// 对: new ArrayList<T>();  或 Array.newInstance(clazz, n) 反射造
坑 4 · varargs + 泛型堆污染 — <T> void f(T... args) 底层是 T[] 原始数组, 混入异类元素编译期不报, CCE 在远处爆发. 正解: 不往里塞 Object 时标 @SafeVarargs; 调用方别传原始数组。
// 错: <T> void f(T... args) 底层是 raw T[], 混入异类编译期不报
//     → CCE 在远处爆发
// 对: 不往里塞 Object 时标 @SafeVarargs; 调用方别传原始数组
坑 5 · 擦除后重载冲突 — void f(List<String>) 与 void f(List<Integer>) 报 "name clash: have same erasure". 原因: 擦除后都是 f(List). 正解: 改方法名或改参数结构; 返回类型不同也救不了。
// 错: void f(List<String> l); void f(List<Integer> l);
//     → name clash: have same erasure (都是 f(List))
// 对: 改方法名或改参数结构; 返回类型不同也救不了
坑 6 · 反射多出合成方法 — getDeclaredMethods 冒出 access$xxx / bridge 方法, 按名字找方法时撞出意外. 原因: 桥方法是编译器为泛型多态生成的 synthetic 方法. 正解: 过滤 isSynthetic() / isBridge(), 匹配时把返回类型也算进签名。
// 错: getDeclaredMethods 按名字找, 撞上 access$xxx / bridge 方法
// 对: 过滤 m.isSynthetic() || m.isBridge();
//     匹配时返回类型也算进签名
坑 7 · 原始类型混用绕过检查 — List list = new ArrayList<String>(); list.add(42); 能编译, 读方 ClassCastException. 原因: raw 类型关闭泛型检查, unchecked 只是警告. 正解: 永远带参数类型; 遗留处用 @SuppressWarnings("rawtypes") 圈住最小范围并注释原因。
// 错: List raw = new ArrayList<String>(); raw.add(42);
//     能编译 (unchecked 警告), 读方 ClassCastException
// 对: 永远带参数类型; 遗留处最小范围 @SuppressWarnings("rawtypes")
坑 8 · 通配符方向搞反 — List<? extends Number> l; l.add(1); 编译错. 原因: extends 是生产者 (只读), 编译器不知道确切类型所以禁止任何 add. 正解: 要写就用具体类型或 ? super Integer — 方向反了不是 bug, 是编译器在拦你。
// 错: List<? extends Number> l; l.add(1);  → 编译错 (生产者只读)
// 对: 要写就用具体类型或 List<? super Integer>
坑 9 · varargs 传 null 数组 — Collections.addAll(list, (String[]) null) 直接 NPE, 而 Arrays.asList(null) 却生成 [null] — 行为不一致埋排查坑. 原因: 前者遍历前就解引用数组取 length. 正解: 判空再传, 空集合用 Collections.emptyList() 兜底。
// 错: Collections.addAll(list, (String[]) null);  → NPE
//     而 Arrays.asList(null) 却生成 [null], 行为不一致
// 对: 判空再传; 空集合用 Collections.emptyList() 兜底
坑 10 · 泛型不变误会 — List<Object> objs = new ArrayList<String>(); 编译错. 原因: 泛型子类型关系是不变 — 若允许, 就能 put 异类穿透类型系统 (数组协变的 ArrayStoreException 就是前车之鉴). 正解: 只读协变用 List<? extends Object>。
// 错: List<Object> objs = new ArrayList<String>();  → 编译错 (不变)
// 对: 只读协变 List<? extends Object>;
//     数组协变的 ArrayStoreException 就是前车之鉴
坑 11 · catch 泛型类型 — catch (T e) 编译错. 原因: 异常匹配是运行时行为, 擦除后等于 catch(Object), 泛型保证是幻觉. 正解: 见场景 9 的函数式走私包装。
// 错: try { ... } catch (T e) { }  → 编译错: 泛型类不能出现在 catch
// 对: 函数式走私 unchecked(() -> ...) 包装 (见场景 9)
坑 12 · 拿不到 List<String>.class — 字节码里不存在这个 Class 对象, 写了直接编译错. 原因: 擦除. 正解: 超类型令牌 (匿名子类 + getGenericSuperclass), Jackson TypeReference 现成可用。
// 错: Class<List<String>> c = List<String>.class;  → 编译错 (擦除)
// 对: new TypeReference<List<String>>() {} 超类型令牌;
//     Jackson TypeReference / Guava TypeToken 现成可用
坑 13 · 链式调用推断失败 — Matcher.nonNull().and(...) 报类型不确定/推断错误. 原因: 链式起点没有目标类型上下文, 编译器推不出 T. 正解: 显式类型见证 Matcher.<Order>nonNull()。
// 错: Matcher.nonNull().and(...);  链式起点无目标类型, 推不出 T
// 对: 显式类型见证 Matcher.<Order>nonNull().and(eq(paid));
坑 14 · @SafeVarargs 放错位置 — 标在可被覆写的实例方法上报错. 原因: 老版本只允许 final/static/private (Java 9 放开到 private 构造器) — 可覆写意味着子类可破坏承诺. 正解: 方法 final 化, 或重新设计 API。
// 错: @SafeVarargs 标在可覆写的实例方法上 → 报错
//     (只允许 final/static/private; 可覆写=子类可破坏承诺)
// 对: 方法 final 化, 或重新设计 API
坑 15 · 通配符捕获报错难读 — l.set(0, l.get(0)) 报 "capture of ?" 相关错误. 原因: 编译器给每个 ? 起了匿名捕获类型, 同一变量两处 get 被视为不同类型. 正解: 写私有泛型 helper <T> void selfSet(List<T> l) 让捕获发生一次。
// 错: l.set(0, l.get(0));  → capture-of-? 两处类型不同, 编译错
// 对: 私有 helper <T> void selfSet(List<T> l) { l.set(0, l.get(0)); }
坑 16 · 协变覆写的桥方法惊吓 — 子类覆写返回更具体类型 (合法), 反射按名找方法却匹配到桥方法, invoke 行为诡异. 原因: 桥方法与原方法仅返回类型不同, 粗放匹配撞车. 正解: getMethods 后过滤 isBridge(), 精确匹配含返回类型的完整签名。
// 错: getMethods 粗放按名匹配, 撞到 bridge 方法 invoke 行为诡异
// 对: 过滤 m.isBridge(); 精确匹配含返回类型的完整签名
坑 17 · 注解里表达不了泛型 — @Cache(type = List<User>.class) 编译错. 原因: 注解成员只允许 Class/基本/String/Enum/注解/数组, 泛型 Class 不存在. 正解: 嵌套注解数组模拟, 或字符串 + 超类型令牌解析。
// 错: @Cache(type = List<User>.class)  → 编译错: 参数化 Class 不存在
// 对: 嵌套注解数组模拟; 或字符串 + 超类型令牌解析
坑 18 · @SuppressWarnings 掩盖堆污染 — unchecked 警告一压, 类型破坏静默进入运行时, CCE 在提交/序列化时才爆. 原因: 警告就是"这里类型契约可能已破". 正解: 标注圈住最小范围 + 注明原因, code review 把新增 unchecked 当 blocker 过。
// 错: 方法级 @SuppressWarnings("unchecked") 一压了之
//     → 类型破坏静默进运行时, CCE 到提交/序列化才爆
// 对: 标注圈最小范围+注明原因; 新增 unchecked 当 blocker 过 review
坑 19 · List<int> 不存在 — 泛型不吃原始类型, 只能 List<Integer> 装箱, 热路径开销陡增. 原因: 擦除要求引用类型. 正解: 数值密集用 IntStream/int[]/第三方特化 Map (如 fastutil)。
// 错: List<int> nums;  → 编译错: 只能 List<Integer> 装箱, 热路径开销陡增
// 对: 数值密集用 IntStream / int[] / fastutil 特化 Map
坑 20 · Comparable<T> 边界写死过严 — <T extends Comparable<T>> 让"父类实现比较器"的子类直接编译不过. 原因: 边界要求 T 精确自比. 正解: 全 JDK 标准写法 <T extends Comparable<? super T>>。
// 错: <T extends Comparable<T>> T max(...)
//     父类实现比较器的子类直接编译不过
// 对: <T extends Comparable<? super T>>  全 JDK 标准写法