【Java踩坑笔记】【基础语法篇】07_泛型擦除:为什么ListString和ListInteger是一家人?

📅 2026/6/27 2:17:32
【Java踩坑笔记】【基础语法篇】07_泛型擦除:为什么ListString和ListInteger是一家人?
摘要编译期有泛型运行期没泛型。泛型擦除是 Java 为了向后兼容做出的妥协但也带来了instanceof不能用、反射受限等一系列坑。一、问题现象publicclassGenericErasure{publicstaticvoidmain(String[]args){ListStringstringsnewArrayList();ListIntegerintegersnewArrayList();System.out.println(strings.getClass()integers.getClass());// trueSystem.out.println(strings.getClass());// class java.util.ArrayList}}运行结果true class java.util.ArrayList「明明泛型参数不同为什么getClass()结果一样」再看一个更迷惑的ListStringlistnewArrayList();list.add(hello);// 绕过编译器泛型检查反射MethodaddMethodlist.getClass().getMethod(add,Object.class);addMethod.invoke(list,123);addMethod.invoke(list,newDate());System.out.println(list.get(1));// 123Integer 类型System.out.println(list.get(2));// Date 对象// 编译器以为 list 里全是 String但实际上什么都有Stringslist.get(0);// ✅Strings2list.get(1);// ❌ ClassCastException运行时二、踩坑现场场景 1instanceof不能用泛型// ❌ 编译错误if(objinstanceofListString){// 编译报错// ...}报错信息Illegal generic type for instanceof场景 2反射获取泛型参数失败publicclassGenericDaoT{privateClassTentityClass;publicGenericDao(){// ❌ 运行期拿不到 T 的具体类型TypesuperClassgetClass().getGenericSuperclass();ParameterizedTypeparamType(ParameterizedType)superClass;entityClass(ClassT)paramType.getActualTypeArguments()[0];// 运行时 T 已被擦除为 Object强制转换会出错}}场景 3泛型数组创建失败// ❌ 编译错误ListString[]arraysnewArrayListString[10];// 编译报错// 原因数组在运行期检查元素类型但泛型已被擦除无法检查三、原理解析3.1 什么是泛型擦除Type ErasureJava 的泛型是编译期的语法糖运行期不存在。编译期编译器做泛型检查不允许往 ListString 里加 Integer ↓ 字节码泛型信息被擦除替换成上限类型通常是 Object ↓ 运行期ArrayListString 和 ArrayListInteger 都是 ArrayListObject擦除规则泛型声明擦除后TObjectT extends NumberNumberT extends Comparable SerializableComparable第一个上限3.2 为什么 Java 要用泛型擦除根本原因向后兼容。Java 5 才引入泛型但 JDK 1.4 及之前的代码里已经有大量List、Map的使用。如果泛型在运行期保留所有旧代码都需要重新编译代价不可接受。于是 Java 选择了「编译期泛型 运行期擦除」的折中方案。3.3 桥方法Bridge Method泛型擦除带来一个问题重写的方法签名对不上。// 源码classMyComparatorimplementsComparatorString{Overridepublicintcompare(Stringa,Stringb){...}}擦除后// 擦除后的等价代码编译器自动生成classMyComparatorimplementsComparator{publicintcompare(Stringa,Stringb){...}// 编译器自动生成的桥方法publicintcompare(Objecta,Objectb){returncompare((String)a,(String)b);// 强制转换}}桥方法是编译器在字节码层面自动生成的用来保证多态正确工作。3.4 编译期泛型检查的本质ListStringlistnewArrayList();list.add(hello);// ✅ 编译通过list.add(123);// ❌ 编译错误// 编译后擦除泛型ListlistnewArrayList();list.add(hello);list.add(123);// 编译后的字节码里这行是合法的泛型安全是编译器给你的承诺运行期不保证。四、正确写法4.1 获取泛型参数的正确方式超类技巧// ✅ 通过继承保留泛型信息publicabstractclassGenericDaoT{privatefinalClassTentityClass;protectedGenericDao(){TypesuperClassgetClass().getGenericSuperclass();ParameterizedTypeparamType(ParameterizedType)superClass;this.entityClass(ClassT)paramType.getActualTypeArguments()[0];}}// 子类必须显式指定泛型参数才能保留到运行期publicclassUserDaoextendsGenericDaoUser{// User 的泛型信息会保留在 UserDao 的 Class 对象里}4.2 绕不开反射时的防御性编程// ✅ 从泛型集合取元素时显式检查类型publicvoidprocess(List?list){for(Objectobj:list){if(objinstanceofString){Strings(String)obj;// ...}}}4.3 不能用instanceof泛型用通配符// ❌ 不能这样if(objinstanceofListString){}// ✅ 只能判断到原始类型if(objinstanceofList){List?list(List?)obj;// 遍历时逐个判断元素类型}4.4 创建泛型数组的替代方案// ❌ 不能直接创建泛型数组// ListString[] arr new ListString[10];// ✅ 用 List 包装ListListStringlistsnewArrayList();lists.add(newArrayList());// ✅ 或者用反射不推荐仅供理解ListString[]arr(ListString[])newList[10];五、最佳实践✅ 泛型的 5 条使用规范泛型只在编译期有效运行期拿不到泛型类型instanceof不能用于泛型只能判断原始类型用「超类技巧」在运行期获取泛型参数反射向泛型集合插入元素时做好类型守卫优先用ListListString代替ListString[] PECS 原则Producer Extends, Consumer Super// ✅ 读取用 extends生产者publicvoidread(List?extendsNumberlist){for(Numbern:list){...}// 可以读// list.add(1); ❌ 不能写除了 null}// ✅ 写入用 super消费者publicvoidwrite(List?superIntegerlist){list.add(1);// 可以写 Integer// Integer i list.get(0); ❌ 读出来是 Object}️ 阿里巴巴 Java 开发手册规约【强制】泛型通配符? extends T来接收返回的数据此写法的泛型集合不能使用add方法而? super T不能使用get方法作为接口调用赋值时易出错。【强制】抽象类命名使用Abstract或Base开头。六、小结Java 泛型是编译期语法糖运行期类型信息被擦除Type Erasure擦除后用上限类型替换无上限则为Object因此ListString.class ListInteger.class编译器通过生成桥方法保证重写的正确性运行期获取泛型类型需要通过「超类技巧」子类显式指定泛型参数instanceof不能用于泛型数组不能用泛型——这是擦除的直接后果下一篇预告枚举的ordinal()别乱用真的会炸—— 枚举序号变了用ordinal()做的判断全错了这个坑在生产环境见过好几次。