Java 擦除如何影响通用数组?

How is the Java erasure affecting the generic arrays?

这段时间在研究泛型,今天为我找到了这个奥秘。

让我们考虑以下虚拟对象 class:

    public class Main{

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

        c.getArray();                      //No Exception
        //c.getArray().getClass();         //Exception
        //int a = c.getArray().length;     //Exception

    } 

}


class Container<T> { 

    T[] array; 

    @SuppressWarnings("unchecked") 
    Container() { 
        array = (T[])new Object[1]; 
    } 

    void put(T item) { 
        array[0] = item; 
    } 

    T get() { return array[0]; } 

    T[] getArray() { return array; }
}  

因为擦除,在运行时,getArray()方法的T[]return类型变成了一个Object[],这对我来说是完全合理的。

如果我们按原样访问该方法 (c.getArray()) 不会抛出异常,但是如果我们尝试在 returned 数组上调用某些方法,例如 c.Array().getClass(),或者如果我们尝试访问一个字段,例如 c.getArray().length,那么会抛出以下异常:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

为什么抛出这个异常? 为什么简单的 c.getArray() 调用也不会抛出它? 如果我们只是调用 getClass() 或访问长度,为什么它会尝试转换为 Integer[]? getClass() 和 length 是否也不适用于 Object[]?

提前感谢您的许多(我希望)和解释性的(我希望也是)答案。

异常的原因是编译器期望 Integer[] 但收到 Object[]。它在 getArray 的调用点添加了 运行 次演员表。那些演员在你的构造函数中发现了说谎的、虚拟的、无效的演员。

为了使其正确,需要 T 的实际 class 才能创建实例。

@SuppressWarnings("unchecked")
Container(Class<T> type) {
    array = (T[]) Array.newInstance(type, 10);
}


    Container<Integer> c = new Container<Integer>(Integer.class); 

    c.getArray();
    Class<?> t = c.getArray().getClass();
    System.out.println(t.getName());
    int a = c.getArray().length;

这里还有一个 "unsafe" 转换为 T[] 但这是不可避免的,因为 Array.newInstance 是 n 维数组的低级方法,例如:

(double[][][][][][]) Array.newInstance(double.class, 3, 3, 3, 3, 3, 6);

我没能在 JLS 中找到说这是行为的确切位置,但我认为原因是这样的:

表达式:

c.getArray().getClass();

大致相当于:

Integer[] arr = (Integer[]) c.getArray();
arr.getClass();

由于类型擦除,必须添加强制转换。此隐式转换在字节码中添加了一条 checkcast 指令,该指令失败并显示 ClassCastException,因为 c.getArray() 的类型为 Object[].

查看字节码:

static void implicit() {
  Container<Integer> c = new Container<Integer>();
  c.getArray().getClass(); //Exception
}

static void explicit() {
  Container<Integer> c = new Container<Integer>();
  Integer[] arr = (Integer[]) c.getArray();
  arr.getClass(); //Exception
}

我们得到:

  static void implicit();
    Code:
       0: new           #2                  // class Container
       3: dup
       4: invokespecial #3                  // Method Container."<init>":()V
       7: astore_0
       8: aload_0
       9: invokevirtual #4                  // Method Container.getArray:()[Ljava/lang/Object;
      12: checkcast     #5                  // class "[Ljava/lang/Integer;"
      15: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      18: pop
      19: return

  static void explicit();
    Code:
       0: new           #2                  // class Container
       3: dup
       4: invokespecial #3                  // Method Container."<init>":()V
       7: astore_0
       8: aload_0
       9: invokevirtual #4                  // Method Container.getArray:()[Ljava/lang/Object;
      12: checkcast     #5                  // class "[Ljava/lang/Integer;"
      15: checkcast     #5                  // class "[Ljava/lang/Integer;"
      18: astore_1
      19: aload_1
      20: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      23: pop
      24: return

所以 explicit 版本的唯一区别是三个指令:

      15: checkcast     #5                  // class "[Ljava/lang/Integer;"
      18: astore_1
      19: aload_1

据我所知,其中哪些只是因为将其显式存储在变量中。

当您执行不安全的未经检查的转换时,它可能会或可能不会在某处导致异常。你不能保证在某处得到异常。

在这种情况下,是否得到异常取决于编译器是否在擦除的代码中插入了一个转换,将调用的结果转换为Integer[]。在这种情况下,似乎在第二和第三种情况下插入了演员表,而不是第一种情况。

在这三种情况下,都允许编译器插入强制转换(因为允许假设结果是Integer[]或者不插入强制转换(因为表达式用在这样的在所有三个中只需要 Object[] 的方式。是否插入强制转换取决于特定的编译器实现来决定。

为什么这个编译器不会在第一种情况下插入强制转换而在第二种和第三种情况下插入强制转换?一个明显的解释是,在第一种情况下,结果显然未被使用,因此确定不需要强制转换非常简单。在第二种和第三种情况下,要确定强制转换是不必要的,需要查看表达式的使用方式以了解它是否也适用于 Object[];这是一个相当复杂的分析。编译器作者可能选择了一种简单的方法,即仅在结果未使用时才跳过转换。

另一个编译器可能会在所有三种情况下插入强制转换。而另一个编译器可能在所有三种情况下都没有强制转换。你不能依赖它。