很多初学者接触 Java 泛型时,会以为 List 和 List 在运行时仍是两个完全不同的类型。其实,编译后的字节码通常看不到尖括号里的类型信息,这种设计叫作类型擦除。编译器先利用泛型做检查和推断,再把类型参数替换成上界,没有显式上界时替换为 Object,并在需要的位置插入强制类型转换。这样既兼容旧版本字节码,也让同类泛型只保留一份运行时代码。
擦除后,不能直接判断 ArrayList 的类型。list instanceof ArrayList 可以成立,但 list instanceof ArrayList 无法通过编译,因为运行时没有 String 这个泛型实参可供判断。也不能创建泛型数组。Java 允许声明 T[],却通常不能写 new T[10],原因是运行时不知道 T 究竟是什么类型,而数组必须知道元素类型才能做安全检查。
泛型类的类型参数属于实例层面的信息,静态字段和静态方法不能直接使用它。例如泛型类中的 static T value 不合法。不同实例共享同一份静态数据,如果静态成员依赖 T,ArrayList 和 ArrayList 就会争夺同一份存储,类型安全也无法维持。理解这一点,能解释不少看似奇怪的编译错误。
擦除还会影响方法重载。由于擦除后 List 和 List 都可能变成 List,只写参数分别为这两种类型的方法,编译器往往会认为签名重复。遇到这类需求,可以改变参数数量、增加包装类型,或使用不同方法名。字节码的方法描述符并不保留完整泛型实参,因此无法靠泛型参数绕过重载规则。
泛型不能使用基本类型作为类型参数,不能写 List,只能写 List。编译时会自动装箱和拆箱,使用方便,但大量运算可能产生额外对象和性能开销。集合保存基本类型时,要意识到真正存储的是包装类型,涉及空值、缓存和对象身份时要格外谨慎。
通配符是配合擦除使用的重要工具。List<?> 可以读取为 Object,却不能随意写入,因为编译器不知道实际元素类型。List<? extends Number> 适合读取 Number 及其子类,List<? super Integer> 适合写入 Integer。这就是生产者使用 extends,消费者使用 super。它不能恢复运行时泛型信息,但能表达更准确的读写约束,减少类型转换错误。
如果确实需要保留泛型信息,可以显式传入 Class,或使用 TypeReference、ParameterizedType 等反射工具。例如 Jackson 反序列化复杂泛型时,就需要借助这些信息。不过这类方案会增加复杂度,也可能失去部分编译期检查,应只在边界场景使用。
总的来说,泛型擦除牺牲了运行时类型信息,换来了兼容性和较简单的实现。写业务代码时,应把泛型当作编译期约束工具,尽量通过清晰的类型声明、通配符和封装让错误在编译阶段暴露。只要理解擦除规则,就能明白哪些写法合法、哪些限制无法绕过,也能避免把类型安全问题留到运行之后。