1. 概述

本文将深入探讨 Java 泛型中的一个核心机制——类型擦除(Type Erasure)。这个机制虽然常被开发者忽略,但理解它对写出健壮的泛型代码至关重要。

2. 什么是类型擦除?

类型擦除的本质是:只在编译时检查类型约束,运行时丢弃所有类型信息。

简单来说,编译器会在编译阶段:

  • 验证类型安全性
  • 插入必要的类型转换
  • 然后彻底擦除泛型类型信息

来看个例子:

public static <E> boolean containsElement(E[] elements, E element) {
    for (E e : elements) {
        if (e.equals(element)) {
            return true;
        }
    }
    return false;
}

编译后实际变成这样:

public static boolean containsElement(Object[] elements, Object element) {
    for (Object e : elements) {
        if (e.equals(element)) {
            return true;
        }
    }
    return false;
}

所有泛型类型 E 都被替换为 Object,这就是类型擦除的直观体现。

3. 类型擦除的分类

类型擦除主要发生在两个层面:类级别方法级别

3.1. 类级别类型擦除

编译器会擦除类上的类型参数,规则如下:

  • 有边界(bound)时,替换为第一个边界类型
  • 无边界时,替换为 Object

Stack 示例说明:

public class Stack<E> {
    private E[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}

编译后变成:

public class Stack {
    private Object[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (Object[]) new Object[capacity];
    }

    public void push(Object data) {
        // ..
    }

    public Object pop() {
        // ..
    }
}

如果类型参数有边界:

public class BoundStack<E extends Comparable<E>> {
    private E[] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}

编译后 E 被替换为第一个边界 Comparable

public class BoundStack {
    private Comparable[] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (Comparable[]) new Object[capacity];
    }

    public void push(Comparable data) {
        // ..
    }

    public Comparable pop() {
        // ..
    }
}

3.2. 方法级别类型擦除

方法上的类型参数同样会被擦除:

  • 无边界 → 替换为 Object
  • 有边界 → 替换为第一个边界类型

打印数组的方法示例:

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

编译后:

public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.printf("%s ", element);
    }
}

有边界的情况:

public static <E extends Comparable<E>> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

编译后:

public static void printArray(Comparable[] array) {
    for (Comparable element : array) {
        System.out.printf("%s ", element);
    }
}

4. 边界情况与陷阱

类型擦除有时会导致一些"坑",特别是涉及继承和方法重写时。

4.1. 桥接方法(Bridge Methods)

当子类继承泛型父类时,编译器可能生成桥接方法来保持多态性。

看这个例子:

public class IntegerStack extends Stack<Integer> {
    public IntegerStack(int capacity) {
        super(capacity);
    }

    public void push(Integer value) {
        super.push(value);
    }
}

现在执行这段代码:

IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack;  // 注意这里用原始类型
stack.push("Hello");         // 竟然能编译通过!
Integer data = integerStack.pop();  // 运行时抛 ClassCastException

类型擦除后,Stack.push() 变成 push(Object),所以 stack.push("Hello") 能通过编译。但在运行时:

  • IntegerStack 继承了 push(Object) 方法
  • 实际调用的是父类方法,导致 String 被塞入 IntegerStack
  • pop() 时类型转换失败

解决方案:编译器自动生成桥接方法:

public class IntegerStack extends Stack {
    // 编译器生成的桥接方法
    public void push(Object value) {
        push((Integer) value);  // 转型为 Integer
    }

    // 原始方法
    public void push(Integer value) {
        super.push(value);
    }
}

这样当调用 push(Object) 时,会通过桥接方法正确调用 push(Integer),保证类型安全。

⚠️ 关键点

  • 桥接方法是编译器自动生成的合成方法
  • 它们确保泛型类型擦除后仍能保持多态性
  • 在字节码中可见,但源码中看不到

5. 总结

类型擦除是 Java 泛型的核心机制,总结要点:

核心原则

  • 泛型只在编译期有效
  • 运行时所有类型参数被擦除为 Object 或边界类型
  • 编译器自动插入类型转换保证安全

实际影响

  • 泛型类不能使用基本类型(如 List<int> 不合法)
  • 不能实例化泛型类型(如 new E()
  • 运行时无法获取具体类型(如 List<String>.class 不存在)

常见误区

  • 误以为运行时保留泛型信息(可通过反射部分获取)
  • 误以为泛型数组是类型安全的(实际需要特殊处理)

深入理解类型擦除能帮你:

  • 避免泛型相关的运行时错误
  • 正确使用泛型 API 设计
  • 理解框架(如 Spring、Jackson)中的泛型处理机制

延伸阅读


原始标题:Type Erasure in Java Explained | Baeldung