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

摘要 :编译期有泛型,运行期没泛型。泛型擦除是 Java 为了向后兼容做出的妥协,但也带来了 instanceof 不能用、反射受限等一系列坑。


一、问题现象

java 复制代码
public class GenericErasure {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();

        System.out.println(strings.getClass() == integers.getClass());  // true!
        System.out.println(strings.getClass());  // class java.util.ArrayList
    }
}

运行结果:

复制代码
true
class java.util.ArrayList

「明明泛型参数不同,为什么 getClass() 结果一样?」

再看一个更迷惑的:

java 复制代码
List<String> list = new ArrayList<>();
list.add("hello");

// 绕过编译器泛型检查(反射)
Method addMethod = list.getClass().getMethod("add", Object.class);
addMethod.invoke(list, 123);
addMethod.invoke(list, new Date());

System.out.println(list.get(1));  // 123(Integer 类型!)
System.out.println(list.get(2));  // Date 对象!

// 编译器以为 list 里全是 String,但实际上什么都有
String s = list.get(0);  // ✅
String s2 = list.get(1); // ❌ ClassCastException(运行时)

二、踩坑现场

场景 1:instanceof 不能用泛型

java 复制代码
// ❌ 编译错误
if (obj instanceof List<String>) {  // 编译报错!
    // ...
}

报错信息:Illegal generic type for instanceof

场景 2:反射获取泛型参数失败

java 复制代码
public class GenericDao<T> {
    private Class<T> entityClass;

    public GenericDao() {
        // ❌ 运行期拿不到 T 的具体类型
        Type superClass = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) superClass;
        entityClass = (Class<T>) paramType.getActualTypeArguments()[0];
        // 运行时 T 已被擦除为 Object,强制转换会出错
    }
}

场景 3:泛型数组创建失败

java 复制代码
// ❌ 编译错误
List<String>[] arrays = new ArrayList<String>[10];  // 编译报错!

// 原因:数组在运行期检查元素类型,但泛型已被擦除,无法检查

三、原理解析

3.1 什么是泛型擦除(Type Erasure)

Java 的泛型是编译期的语法糖,运行期不存在。

复制代码
编译期:编译器做泛型检查(不允许往 List<String> 里加 Integer)
       ↓
字节码:泛型信息被"擦除",替换成上限类型(通常是 Object)
       ↓
运行期:ArrayList<String> 和 ArrayList<Integer> 都是 ArrayList<Object>

擦除规则

泛型声明 擦除后
<T> Object
<T extends Number> Number
<T extends Comparable & Serializable> Comparable(第一个上限)

3.2 为什么 Java 要用泛型擦除?

根本原因:向后兼容。

Java 5 才引入泛型,但 JDK 1.4 及之前的代码里已经有大量 ListMap 的使用。如果泛型在运行期保留,所有旧代码都需要重新编译,代价不可接受。

于是 Java 选择了「编译期泛型 + 运行期擦除」的折中方案。

3.3 桥方法(Bridge Method)

泛型擦除带来一个问题:重写的方法签名对不上。

java 复制代码
// 源码
class MyComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) { ... }
}

擦除后:

java 复制代码
// 擦除后的等价代码(编译器自动生成)
class MyComparator implements Comparator {
    public int compare(String a, String b) { ... }

    // 编译器自动生成的桥方法
    public int compare(Object a, Object b) {
        return compare((String) a, (String) b);  // 强制转换
    }
}

桥方法是编译器在字节码层面自动生成的,用来保证多态正确工作。

3.4 编译期泛型检查的本质

java 复制代码
List<String> list = new ArrayList<>();
list.add("hello");   // ✅ 编译通过
list.add(123);       // ❌ 编译错误

// 编译后(擦除泛型)
List list = new ArrayList<>();
list.add("hello");
list.add(123);  // 编译后的字节码里,这行是合法的!

泛型安全是编译器给你的承诺,运行期不保证。


四、正确写法

4.1 获取泛型参数的正确方式(超类技巧)

java 复制代码
// ✅ 通过继承保留泛型信息
public abstract class GenericDao<T> {
    private final Class<T> entityClass;

    protected GenericDao() {
        Type superClass = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) superClass;
        this.entityClass = (Class<T>) paramType.getActualTypeArguments()[0];
    }
}

// 子类必须显式指定泛型参数,才能保留到运行期
public class UserDao extends GenericDao<User> {
    // User 的泛型信息会保留在 UserDao 的 Class 对象里
}

4.2 绕不开反射时的防御性编程

java 复制代码
// ✅ 从泛型集合取元素时,显式检查类型
public void process(List<?> list) {
    for (Object obj : list) {
        if (obj instanceof String) {
            String s = (String) obj;
            // ...
        }
    }
}

4.3 不能用 instanceof 泛型?用通配符

java 复制代码
// ❌ 不能这样
if (obj instanceof List<String>) { }

// ✅ 只能判断到原始类型
if (obj instanceof List) {
    List<?> list = (List<?>) obj;
    // 遍历时逐个判断元素类型
}

4.4 创建泛型数组的替代方案

java 复制代码
// ❌ 不能直接创建泛型数组
// List<String>[] arr = new List<String>[10];

// ✅ 用 List 包装
List<List<String>> lists = new ArrayList<>();
lists.add(new ArrayList<>());

// ✅ 或者用反射(不推荐,仅供理解)
List<String>[] arr = (List<String>[]) new List[10];

五、最佳实践

✅ 泛型的 5 条使用规范

  1. 泛型只在编译期有效,运行期拿不到泛型类型
  2. instanceof 不能用于泛型,只能判断原始类型
  3. 用「超类技巧」在运行期获取泛型参数
  4. 反射向泛型集合插入元素时,做好类型守卫
  5. 优先用 List<List<String>> 代替 List<String>[]

🔍 PECS 原则(Producer Extends, Consumer Super)

java 复制代码
// ✅ 读取用 extends(生产者)
public void read(List<? extends Number> list) {
    for (Number n : list) { ... }  // 可以读
    // list.add(1);  ❌ 不能写(除了 null)
}

// ✅ 写入用 super(消费者)
public void write(List<? super Integer> list) {
    list.add(1);  // 可以写 Integer
    // Integer i = list.get(0);  ❌ 读出来是 Object
}

🛠️ 阿里巴巴 Java 开发手册规约

【强制】 泛型通配符 <? extends T> 来接收返回的数据,此写法的泛型集合不能使用 add 方法,而 <? super T> 不能使用 get 方法,作为接口调用赋值时易出错。

【强制】 抽象类命名使用 AbstractBase 开头。


六、小结

  • Java 泛型是编译期语法糖,运行期类型信息被擦除(Type Erasure)
  • 擦除后用上限类型替换(无上限则为 Object),因此 List<String>.class == List<Integer>.class
  • 编译器通过生成桥方法保证重写的正确性
  • 运行期获取泛型类型需要通过「超类技巧」(子类显式指定泛型参数)
  • instanceof 不能用于泛型,数组不能用泛型------这是擦除的直接后果

下一篇预告:枚举的 ordinal() 别乱用,真的会炸 ------ 枚举序号变了,用 ordinal() 做的判断全错了,这个坑在生产环境见过好几次。