泛型只活在编译期 — 擦除后字节码里只有一个 raw Box; PECS 通配符口诀; 桥方法与运行时取类型的三板斧
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 设计里最值钱的信息。
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 String。等价于编译器替你写了强转, 且检查过才让你写。Box<String> box = ...; String s = box.get(); // 字节码: get() 返回 Object, 紧跟 checkcast String // 关键: 强转是编译器替你写的, 且检查过才让你写
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>
class StrNode extends Node<String> { @Override public String data() { ... } // 覆写为具体类型 } // 编译器合成: bridge Object data() { return data(); } 保多态 // 关键: getDeclaredMethods 多出的 synthetic 桥方法不是 bug
? 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 raw = new ArrayList<String>(); // raw: 关掉泛型检查 raw.add(42); // 编译过, 仅 unchecked 警告 String s = (String) raw.get(0); // → CCE 在调用方爆
List<Object> 不是 List<String> 的父类(不变, 编译期检查) — 泛型数组因此被禁止。Object[] a = new String[1]; // 数组协变: 编译过 a[0] = 42; // → ArrayStoreException 运行时爆 // List<Object> l = new ArrayList<String>(); 编译错: 泛型不变
@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
<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 不存在
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)); // 链式起点无目标类型时必用 // 关键: 见证只写在"推断失败"的那一环
网关统一响应体, 泛型让调用方拿到的是具体类型而不是 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> 的接口, 就是团队里"类型黑洞"的起点。
请求上下文要装各种类型的属性, 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);
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 通道 — 用令牌补救。
继承链上的 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 同原理
十个 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); } }
校验/转换逻辑散在五个 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);
要写一个能把 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 灵活性的分水岭。
写通用 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> 也能喂进来
线上 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)); // 一行搞定
测试断言库不想引依赖 — 接口 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> 类型见证: 链式起点没有目标类型, 必须显式帮推断一把。
instanceof List (raw, 配 unchecked 警告) 或逐元素判类型。// 错: if (o instanceof List<String>) {} → illegal generic type for instanceof // 对: if (o instanceof List<?> l) {} 再逐元素判类型
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> 参数
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) 反射造
<T> void f(T... args) 底层是 T[] 原始数组, 混入异类元素编译期不报, CCE 在远处爆发. 正解: 不往里塞 Object 时标 @SafeVarargs; 调用方别传原始数组。// 错: <T> void f(T... args) 底层是 raw T[], 混入异类编译期不报 // → CCE 在远处爆发 // 对: 不往里塞 Object 时标 @SafeVarargs; 调用方别传原始数组
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)) // 对: 改方法名或改参数结构; 返回类型不同也救不了
isSynthetic() / isBridge(), 匹配时把返回类型也算进签名。// 错: getDeclaredMethods 按名字找, 撞上 access$xxx / bridge 方法 // 对: 过滤 m.isSynthetic() || m.isBridge(); // 匹配时返回类型也算进签名
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")
List<? extends Number> l; l.add(1); 编译错. 原因: extends 是生产者 (只读), 编译器不知道确切类型所以禁止任何 add. 正解: 要写就用具体类型或 ? super Integer — 方向反了不是 bug, 是编译器在拦你。// 错: List<? extends Number> l; l.add(1); → 编译错 (生产者只读) // 对: 要写就用具体类型或 List<? super Integer>
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() 兜底
List<Object> objs = new ArrayList<String>(); 编译错. 原因: 泛型子类型关系是不变 — 若允许, 就能 put 异类穿透类型系统 (数组协变的 ArrayStoreException 就是前车之鉴). 正解: 只读协变用 List<? extends Object>。// 错: List<Object> objs = new ArrayList<String>(); → 编译错 (不变) // 对: 只读协变 List<? extends Object>; // 数组协变的 ArrayStoreException 就是前车之鉴
catch (T e) 编译错. 原因: 异常匹配是运行时行为, 擦除后等于 catch(Object), 泛型保证是幻觉. 正解: 见场景 9 的函数式走私包装。// 错: try { ... } catch (T e) { } → 编译错: 泛型类不能出现在 catch // 对: 函数式走私 unchecked(() -> ...) 包装 (见场景 9)
// 错: Class<List<String>> c = List<String>.class; → 编译错 (擦除) // 对: new TypeReference<List<String>>() {} 超类型令牌; // Jackson TypeReference / Guava TypeToken 现成可用
Matcher.nonNull().and(...) 报类型不确定/推断错误. 原因: 链式起点没有目标类型上下文, 编译器推不出 T. 正解: 显式类型见证 Matcher.<Order>nonNull()。// 错: Matcher.nonNull().and(...); 链式起点无目标类型, 推不出 T // 对: 显式类型见证 Matcher.<Order>nonNull().and(eq(paid));
// 错: @SafeVarargs 标在可覆写的实例方法上 → 报错 // (只允许 final/static/private; 可覆写=子类可破坏承诺) // 对: 方法 final 化, 或重新设计 API
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)); }
isBridge(), 精确匹配含返回类型的完整签名。// 错: getMethods 粗放按名匹配, 撞到 bridge 方法 invoke 行为诡异 // 对: 过滤 m.isBridge(); 精确匹配含返回类型的完整签名
@Cache(type = List<User>.class) 编译错. 原因: 注解成员只允许 Class/基本/String/Enum/注解/数组, 泛型 Class 不存在. 正解: 嵌套注解数组模拟, 或字符串 + 超类型令牌解析。// 错: @Cache(type = List<User>.class) → 编译错: 参数化 Class 不存在 // 对: 嵌套注解数组模拟; 或字符串 + 超类型令牌解析
// 错: 方法级 @SuppressWarnings("unchecked") 一压了之 // → 类型破坏静默进运行时, CCE 到提交/序列化才爆 // 对: 标注圈最小范围+注明原因; 新增 unchecked 当 blocker 过 review
IntStream/int[]/第三方特化 Map (如 fastutil)。// 错: List<int> nums; → 编译错: 只能 List<Integer> 装箱, 热路径开销陡增 // 对: 数值密集用 IntStream / int[] / fastutil 特化 Map
<T extends Comparable<T>> 让"父类实现比较器"的子类直接编译不过. 原因: 边界要求 T 精确自比. 正解: 全 JDK 标准写法 <T extends Comparable<? super T>>。// 错: <T extends Comparable<T>> T max(...) // 父类实现比较器的子类直接编译不过 // 对: <T extends Comparable<? super T>> 全 JDK 标准写法