Type Erasure in Java Explained – Java中的类型擦除详解

最后修改: 2017年 7月 4日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick article, we’ll discuss the basics of an important mechanism in Java’s generics known as type erasure.

在这篇文章中,我们将讨论Java泛型中一个重要机制的基础知识,即类型清除。

2. What Is Type Erasure?

2.什么是类型清除?

Type erasure can be explained as the process of enforcing type constraints only at compile time and discarding the element type information at runtime.

类型清除可以解释为仅在编译时执行类型约束,并在运行时丢弃元素类型信息的过程。

For example:

比如说。

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

The compiler replaces the unbound type E with an actual type of Object:

编译器将非绑定类型E替换为Object的实际类型。

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

Therefore the compiler ensures type safety of our code and prevents runtime errors.

因此,编译器确保了我们代码的类型安全,并防止运行时出错。

3. Types of Type Erasure

3.类型清除的类型

Type erasure can occur at class (or variable) and method levels.

类型擦除可以发生在类(或变量)和方法层面。

3.1. Class Type Erasure

3.1.班级类型擦除

At the class level, the compiler discards the type parameters on the class and replaces them with its first bound, or Object if the type parameter is unbound.

在类的层面上,编译器丢弃类上的类型参数,并以其第一个绑定的类型参数代替,如果类型参数是未绑定的,则以Object代替。

Let’s implement a Stack using an array:

让我们用一个数组来实现一个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() {
        // ..
    }
}

Upon compilation, the compiler replaces the unbound type parameter E with Object:

编译时,编译器将非绑定类型参数E替换为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() {
        // ..
    }
}

In a case where the type parameter E is bound:

在类型参数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() {
        // ..
    }
}

The compiler will replace the bound type parameter E with the first bound class, Comparable in this case:

编译器将用第一个绑定的类来替换绑定的类型参数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. Method Type Erasure

3.2.方法类型擦除

For method-level type erasure, the method’s type parameter is not stored but rather converted to its parent type Object if it’s unbound or it’s first bound class when it’s bound.

对于方法级的类型清除,方法的类型参数不被存储,而是转换为它的父类型Object(如果它是未绑定的)或它的第一个绑定类(当它被绑定时)。

Let’s consider a method to display the contents of any given array:

让我们考虑一种方法来显示任何给定数组的内容。

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

Upon compilation, the compiler replaces the type parameter E with Object:

编译时,编译器将类型参数E替换为Object

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

For a bound method type parameter:

对于一个绑定的方法类型参数。

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

We’ll have the type parameter E erased and replaced with Comparable:

我们将把类型参数E删除,用Comparable:代替。

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

4. Edge Cases

4.边缘案例

Sometime during the type erasure process, the compiler creates a synthetic method to differentiate similar methods. These may come from method signatures extending the same first bound class.

在类型清除过程中的某个时候,编译器会创建一个合成方法来区分类似的方法。这些方法可能来自于扩展同一个第一约束类的方法签名。

Let’s create a new class that extends our previous implementation of Stack. Please note this refers to the Stack class we created in section 3.1, and not java.util.Stack.

让我们创建一个新的类,它扩展了我们之前对Stack的实现。请注意这是指我们在第3.1节中创建的Stack类,而不是java.util.Stack

public class IntegerStack extends Stack<Integer> {

    public IntegerStack(int capacity) {
        super(capacity);
    }

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

Now let’s look at the following code:

现在让我们看一下下面的代码。

IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack;
stack.push("Hello");
Integer data = integerStack.pop();

After type erasure, we have:

经过类型擦除,我们有。

IntegerStack integerStack = new IntegerStack(5);
Stack stack = (IntegerStack) integerStack;
stack.push("Hello");
Integer data = (String) integerStack.pop();

Notice how we can push a String on the IntegerStack – because IntegerStack inherited push(Object) from the parent class Stack. This is, of course, incorrect – as it should be an integer since integerStack is a Stack<Integer> type.

注意到我们如何在String上推送一个IntegerStack – 因为IntegerStack从父类Stack继承了push(Object)。当然,这是不正确的–因为它应该是一个整数,因为integerStack是一个Stack<Integer>类型。

So, not surprisingly, an attempt to pop a String and assign to an Integer causes a ClassCastException from a cast inserted during the push by the compiler.

因此,毫不奇怪,试图pop一个String并赋值给一个Integer会导致一个ClassCastException,这是编译器在push时插入的一个cast。

4.1. Bridge Methods

4.1.桥梁方法

To solve the edge case above, the compiler sometimes creates a bridge method. This is a synthetic method created by the Java compiler while compiling a class or interface that extends a parameterized class or implements a parameterized interface where method signatures may be slightly different or ambiguous.

为了解决上述边缘情况,编译器有时会创建一个桥梁方法。这是一个由Java编译器在编译一个扩展了一个参数化类或实现了一个参数化接口的类或接口时创建的合成方法,其中的方法签名可能略有不同或不明确。

In our example above, the Java compiler preserves polymorphism of generic types after erasure by ensuring no method signature mismatch between IntegerStack‘s push(Integer) method and Stack‘s push(Object) method.

在我们上面的例子中,Java编译器通过确保IntegerStackpush(Integer)方法和Stackpush(Object)方法之间没有方法签名不匹配,从而在擦除后保留了通用类型的多态。

Hence, the compiler creates a bridge method here:

因此,编译器在这里创建了一个桥梁方法。

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

Consequently, Stack class’s push method after type erasure, delegates to the original push method of IntegerStack class.

因此,Stack类的push方法在类型清除后,委托给IntegerStack类的原始push方法。

5. Conclusion

5.结论

In this tutorial, we’ve discussed the concept of type erasure with examples in type parameter variables and methods.

在本教程中,我们已经用类型参数变量和方法中的例子讨论了类型擦除的概念。

You can read more about these concepts:

你可以阅读更多关于这些概念的内容。

As always, the source code that accompanies this article is available over on GitHub.

一如既往,本文所附的源代码可在GitHub上获取。