
「Java 进阶之路」系列 Day26写在前面模块三集合框架写到这篇收官回过头讲讲一直陪着这些集合类出现、却很少被单独拎出来讲透的泛型。很多人知道Java泛型是伪泛型这个说法但问一句伪在哪、具体擦除掉了什么就说不清楚了。这篇把类型擦除的机制和几个由此产生的经典坑讲明白。一、是什么泛型只存在于编译期运行时会被擦除源码里写List of String编译器做类型检查编译后擦除成普通的List运行时JVM只看到List 完全不知道泛型参数是什么Java的泛型是编译期的类型检查机制T这类类型参数只在编译阶段起作用编译器会在这个阶段帮你检查类型是否匹配、该插入自动类型转换的地方插入转换代码一旦编译完成字节码里泛型信息就被擦除了ListString和ListInteger在运行时其实就是同一个List用一段代码可以直接验证ListStringstringListnewArrayList();ListIntegerintListnewArrayList();System.out.println(stringList.getClass()intList.getClass());// true这就是类型擦除这个说法的直接体现——JVM运行时根本不知道也不关心一个List装的是String还是Integer泛型参数信息在编译产出.class文件的那一刻就已经不存在了。二、为什么要这样设计向后兼容是首要考量Java泛型是JDK5才引入的在这之前所有集合类都是直接存Object取出来自己强制转型。如果泛型引入之后彻底改变运行时的类型表示比如像C模板那样为每种类型参数各自生成一份独立的字节码那JDK5之前编译出来的、大量存在的老代码将没法和新代码互相调用——类型擦除本质上是用泛型信息只在编译期存在这种设计换来了新旧代码在运行时层面的无缝兼容。代价是运行时丢失了泛型的具体类型信息由此产生了下面这些容易踩的坑。擦除的具体规则没有限定的类型参数比如T会被擦除成Object有限定的类型参数比如T extends Comparable会被擦除成这个限定的上界也就是Comparable。三、怎么用类型擦除带来的几个经典坑坑一不能直接创建泛型数组classBoxT{// T[] arr new T[10]; // 编译报错Object[]arrnewObject[10];// 只能这样绕过去}数组在运行时需要明确知道自己的元素类型用来做ArrayStoreException这类运行时类型检查但泛型参数T在运行时已经被擦除成了ObjectJVM压根不知道T具体是什么类型没法创建一个运行时类型明确的数组所以这行代码直接被编译器拒绝。坑二静态成员不能使用类的泛型参数classBoxT{// static T value; // 编译报错// static T getValue() { ... } // 编译报错}类的泛型参数T是和具体实例绑定的new BoxString()和new BoxInteger()是两个不同泛型实例化但静态成员属于类本身在类加载的时候就已经存在不依赖任何具体实例——这时候T到底该是String还是Integer根本无从谈起所以类的泛型参数不能用在静态字段和静态方法上。坑三反射能绕开泛型的编译期检查ListStringlistnewArrayList();list.add(hello);try{Methodaddlist.getClass().getMethod(add,Object.class);add.invoke(list,100);// 通过反射硬塞进去一个Integer}catch(Exceptione){}System.out.println(list);// [hello, 100]编译期类型检查形同虚设泛型的类型检查完全是编译器插入的语法糖运行时add方法的真实签名其实是add(Object)T被擦除成了Object。用反射直接拿到这个方法调用跳过了编译器插入的类型检查逻辑照样能把一个类型不匹配的元素塞进声明为ListString的集合里——这也说明了泛型提供的类型安全仅仅是编译期的一层保护不是运行时真正的类型隔离。四、面试追问Q1什么是类型擦除Java泛型为什么被称为伪泛型类型擦除是指泛型的类型参数信息只在编译阶段存在用于编译期的类型检查编译完成后字节码里这些类型参数信息就被擦除掉了没有限定的类型参数擦除成Object有限定的擦除成限定的上界。因为运行时已经不存在真正独立的泛型类型ListString和ListInteger运行时是同一个Class和C模板那种为每种类型生成独立代码的真泛型不同所以Java的泛型常被称为伪泛型。Q2为什么 ListString 和 ListInteger 的 getClass() 返回结果相等因为类型擦除之后两者在运行时其实是同一个类——ArrayList。泛型参数String和Integer只在编译期用于类型检查编译完成后这部分信息就不存在了JVM在运行时看到的只是普通的ArrayList不会区分它当初是用什么泛型参数声明的。Q3为什么不能创建泛型数组比如 new T[10]因为数组在运行时需要明确知道自己的元素类型用来支持类似ArrayStoreException这样的运行时类型检查但泛型参数T在运行时已经被擦除掉了JVM并不知道T具体对应哪个类型无法创建一个运行时类型信息完整的数组所以编译器直接禁止这种写法。Q4为什么类的静态成员不能使用这个类声明的泛型参数因为类的泛型参数是和具体的实例化绑定的比如BoxString和BoxInteger是两种不同的泛型实例化而静态成员属于类本身在类加载阶段就已经存在不依赖任何具体的实例这时候根本没有一个明确的泛型参数可以对应所以类的泛型参数不能用在静态字段和静态方法上。Q5反射为什么能绕过泛型的类型检查往 ListString 里塞进一个 Integer因为泛型提供的类型检查是编译器在编译阶段插入的语法糖add方法真正的运行时签名因为类型擦除已经变成了add(Object)。通过反射直接获取这个方法并调用跳过了编译器在正常调用路径上插入的类型检查逻辑运行时ArrayList本身并不会校验传进来的对象是不是String所以能够成功把类型不匹配的元素塞进去这也说明泛型的类型安全只在编译期有效不是运行时层面真正的类型隔离。下一篇预告模块三集合框架到这篇正式收官。Day27 开始进入模块四异常与IO讲清楚 Checked 异常和 Unchecked 异常的本质区别以及自定义异常该怎么设计才合理。