泛型和类型擦除

时间:2014-10-15 20:33:10

标签: java generics type-erasure

任何人都可以向我解释为什么会发生这种情况:

public class Array<E> {

    public E[] elements = (E[]) new Object[10];

    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);
        Integer bad1 = arr.elements[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) arr.elements[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array<Integer> test = new Array<>();

        Array.doSomething(test);
    }
}

此处的完整示例:http://pastebin.com/et7sGLGW

我已经阅读了有关类型擦除的内容,并且在编译过程中实现了类型检查,然后EObject替换,所以我们只有public Object[] elements,但是为什么get方法在常规类型转换不成功的情况下成功? get的方法返回类型是否也会被删除?

感谢。

5 个答案:

答案 0 :(得分:3)

即使arr的类型为Array<Integer>(且arr.elements的类型为Integer[]),arr.elements实际上也有运行时类型 Object[],因为实际数组是Object[]类型的实例。

(注意,与泛型不同,数组协变,并且有擦除。Object[] foo = new String[5];是合法的 - 和String[] bar = (String[]) foo;一样。而Integer[] baz = (Integer[]) foo;会在运行时引发ClassCastException。)

因此arr.elements的任何引用触发运行时异常的原因是编译器会自动将向下转换为Integer[],以使类型和运行时 - 键入一致。在doSomething的正文中,arr.elements实际上意味着(Integer[]) arr.elements,带有隐式演员。

相比之下,在get()内,this的类型只是Array<E>,因此elements的类型只是E[],无法检查。因此编译器不会插入任何隐式转换。


主要的关键点是(E[]) new Object[10];实际上不正确new Object[10] 创建E[]的实例。编译器无法看到它不正确,但会插入大量看到它不正确的演员表。

更好的方法是使用Object[] elements = new Object[],并在必要时执行从ObjectE的正确但未经检查的强制转换,而不是不正确 - 并且取消选中从Object[]E[]的广告。

你明白我的意思吗?

答案 1 :(得分:2)

注意:此答案涉及您的pastebin链接上的代码。我建议您编辑问题并包含整个代码。时间不长。

此特定代码中的问题是您将doSomething参数arr声明为Array<Integer>类型。所以当你说

    Integer bad1 = arr.elements[0];

由于arrArray<Integer>,即类型参数EInteger,编译器会假定elements的类型,它被声明为{{ 1}},是E[]

但是,当使用Integer[]创建elements时,无论是在构造函数中还是在new中创建append,都会将其创建为temp }:

Object[]

elements = (E[]) new Object[(capacity = 2)];

这意味着在运行时创建数组对象时,其类型将在运行时记录为E[] temp = (E[]) new Object[(capacity *= 2)]; ... elements = temp; 。对Object[]的强制转换不会影响这一点,因为对象的运行时类型永远不会改变。强制转换仅影响编译器查看表达式的方式。

因此,在本声明中:

E[]

这里,编译器&#34;知道&#34;如上所述,Integer bad1 = arr.elements[0]; 应该是elements类型。由于Integer[]的实际运行时类型为elements,并且Object[]无法隐式转换为Object[],因此在运行时会发生Integer[]

ClassCastException

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; 表示数组。)

使用[L时没有发生这种情况的原因是访问数组的代码不在编译器知道的地方。 get()应该是E。因此,它并不假设Integerelements,并且没有进行任何检查。

(我不完全确定仿制药涉及的确切机制,但我认为这是正确的。)

答案 2 :(得分:2)

首先,您有一个Object[]并且您使用

将其作为E[]传递
E[] elements = (E[]) new Object[10];

这是所有问题的罪魁祸首。

为什么这是一个问题?

  1. 您甚至无法创建此类数组。它将编译没有问题,但理论上(并在运行时)这是无效的。以下是Integer[]

    的示例
    Integer[] stringArray = (Integer[])new Object[10];
    

    此行在运行时抛出错误:

    java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    但是,在您必须直接使用数据之前,泛型会隐藏这些问题。

  2. 让我们尝试这段代码:

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.elements[0] = 5;
        }
    }
    

    整个代码编译没有问题,array的初始化按预期工作。但现在我们得到一个新的例外:

    array.elements[0] = 5;
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    这意味着在内部我们仍然使用Object[],但尝试将其作为Integer[],或者更正式地使用E[](这是错误的,如所述在1。)。

  3. 让我们添加一个append方法(改编自OP发布的代码):

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
        private int size = 0;
        public void append(E element) {
            elements[size++] = element;
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.append(5);
            System.out.println(array.elements[0]);
        }
    }
    

    这里,我们将在这里获取运行时错误:

    System.out.println(array.elements[0]);
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    由于上述原因相同。请注意,您的帖子中的三个示例就是这种情况。

  4. 简而言之:

    您不应直接使用E[] array。相反,请使用ArrayList源代码中描述的Object[] array

    更多信息:

答案 3 :(得分:2)

类型擦除会从泛型类中删除泛型类型,但如果需要,它将在您使用泛型类型的位置插入类型转换。在您的示例中,当您使用泛型Array类时,编译器会添加类型转换。但是在通用数组内部发生的E衰变为对象。 (参见javap的评论和输出)。您看到的错误就是编译器抱怨从Object []到Integer []的转换,这是非法的(泛型或非泛型)。

public class Array<E> {
    public E[] elements;
    @SuppressWarnings("unchecked")
    public Array() {
        this.elements = (E[])new Object[]{1,2,3};
    }
    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.                                                        
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);                             // produces (Integer) arr.get(0)                            
        Integer good1 = (Integer) ((Object[])arr.elements)[0]; // no implicit cast                                         
        Integer bad1 = arr.elements[0];                        // produces ((Integer[])arr.elements)[0]                    
        Integer bad2 = ((Integer[]) arr.elements)[0];          // produces ((Integer[])((Integer[])arr.elements))[0]       
        Integer bad3 = (Integer) arr.elements[0];              // produces ((Integer[])arr.elements)[0]                    
        // `bad1', `bad2', and `bad3' all produce a                                                                        
        // runtime exception.                                                                                              
    }

    public static void main(String[] args) throws Exception{
        doSomething(new Array<Integer>());
    }
}

javap -cp的输出。 -c Array

> public static void
> doSomething(Array<java.lang.Integer>);
>     Code:
>        0: aload_0       
>        1: iconst_0      
>        2: invokevirtual #6                  // Method get:(I)Ljava/lang/Object;
>        5: checkcast     #7                  // class java/lang/Integer
>        8: astore_1      
>        9: aload_0       
>       10: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       13: checkcast     #4                  // class "[Ljava/lang/Object;"
>       16: iconst_0      
>       17: aaload        
>       18: checkcast     #7                  // class java/lang/Integer
>       21: astore_2      
>       22: aload_0       
>       23: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       26: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       29: iconst_0      
>       30: aaload        
>       31: astore_3      
>       32: aload_0       
>       33: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       36: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       39: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       42: iconst_0      
>       43: aaload        
>       44: astore        4
>       46: aload_0       
>       47: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       50: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       53: iconst_0      
>       54: aaload        
>       55: astore        5
>       57: return

答案 4 :(得分:1)

在类型擦除之后考虑代码的样子是有益的,因为这显示了所有的强制转换:

public class Array {

    public Object[] elements = new Object[10];

    public Object get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array arr) {
        Integer good = (Integer)arr.get(0);
        Integer bad1 = ((Integer[])arr.elements)[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) ((Integer[])arr.elements)[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array test = new Array();

        Array.doSomething(test);
    }
}

有了这个,很明显为什么会发生强制转换异常。

你可能会问,为什么演员阵容会发生。为什么arr.elements被投放到Integer[]?这是因为在类型擦除后,arr.elements的类型为Object[];但是在方法中,我们正在使用它并期望它为Integer[],因此当它退出通用范围并进入我们具有替换T的特定类型的方法时,必须进行强制转换。 / p>

如果arr.elements被立即传递或分配到需要类型Object[]Object的内容中,则不一定总是进行此强制转换,然后不进行强制转换,因为它是不必要的。但在其他情况下,演员阵容已经完成。

可以说,这不是代码的错误类型擦除:

Integer bad1 = (Integer)arr.elements[0];

如果一个编译器真的很聪明并且它真的想要,那么它可能会像这样编译它。但要做到这一点,编译器需要添加一个额外的复杂规则,关于是否抛出泛型中的内容,以确定它是否可以在数组访问之后将其推迟。另外,从编译器的角度来看,这没有任何好处,因为无论哪种方式都有一个演员。所以编译器不会这样做。