Java泛型中的类型擦除解释 | baeldung


ava 语言中引入了泛型概念,以在编译时提供更严格的类型检查并支持泛型编程。在这篇简短的文章中,我们将讨论 Java 泛型中一种称为类型擦除的重要机制的基础知识。
 
什么是类型擦除?
类型擦除可以解释为仅在编译时强制执行类型约束并在运行时丢弃元素类型信息的过程。
例如:

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

编译器将未绑定的类型E替换为实际类型的Object:
public static  boolean containsElement(Object [] elements, Object element){
    for (Object e : elements){
        if(e.equals(element)){
            return true;
        }
    }
    return false;
}

因此,编译器确保我们代码的类型安全并防止运行时错误。
 
类型擦除的类型
类型擦除可以发生在类(或变量)和方法级别。
  • 类级别

在类级别,编译器丢弃类上的类型参数,并用其第一个绑定替换它们,如果类型参数未绑定,则使用Object替换它们。
让我们使用数组来实现一个堆栈:
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() {
       
// ..
    }
}

在编译时,编译器用Object取代了未绑定类型参数:
public class Stack {
    private Object[] stackContent;

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

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

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

在类型参数E被绑定的情况下:
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() {
       
// ..
    }
}

  • 方法类型擦除

对于方法级类型擦除,如果方法未绑定或绑定时为第一个绑定类,则不会存储该方法的类型参数,而是将其转换为其父类型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);
    }
}

编译器将删除类型参数E并替换为Comparable:
public static void printArray(Comparable[] array) {
    for (Comparable element : array) {
        System.out.printf("%s ", element);
    }
}

 

边缘情况
在类型擦除过程中的某个时候,编译器会创建一个合成方法来区分相似的方法。这些可能来自扩展相同第一个绑定类的方法签名。

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();

类型擦除后,我们应该能够:
IntegerStack integerStack = new IntegerStack(5);
Stack stack = (IntegerStack) integerStack;
stack.push("Hello");
Integer data = (String) integerStack.pop();

因为integerStack是一个Stack<Integer>类型,最后一行会导致ClassCastException
 
桥接方法
为了解决上述边缘情况,编译器有时会创建桥接方法。这是 Java 编译器在编译扩展参数化类或实现参数化接口的类或接口时创建的合成方法,其中方法签名可能略有不同或不明确。
在我们上面的示例中,Java 编译器通过确保IntegerStack的push(Integer)方法和Stack的push(Object)方法之间没有方法签名不匹配来保留擦除后泛型类型的多态性。
因此,编译器在这里创建了一个桥接方法:

public class IntegerStack extends Stack {
    // Bridge method generated by the compiler
    
    public void push(Object value) {
        push((Integer)value);
    }

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

因此,类型擦除后Stack类的push方法委托给IntegerStack类的原始push方法。
 
结论:
Java 语言中引入了泛型概念,以在编译时提供更严格的类型检查并支持泛型编程。实现泛型的方式,Java 编译器将类型擦除应用于:
  • 如果类型参数是没有绑定的,则将泛型类型中的所有类型参数替换为其绑定类型或Object。因此,生成的字节码只包含普通的类、接口和方法。
  • 必要时插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。