为什么我不能在Java中创建类型参数的数组?

Zhu*_* Li 17 java arrays generics

好吧,我已经阅读了很多这个问题的答案,但我有一个更具体的问题.以下面的代码片段为例.

public class GenericArray<E>{
    E[] s= new E[5];
}
Run Code Online (Sandbox Code Playgroud)

在类型擦除之后,它变成了

public class GenericArray{
    Object[] s= new Object[5];
}
Run Code Online (Sandbox Code Playgroud)

这段代码似乎运作良好.为什么会导致编译时错误?

此外,我从其他答案中得知,以下代码适用于同一目的.

public class GenericArray<E>{
    E[] s= (E[])new Object[5];
}
Run Code Online (Sandbox Code Playgroud)

我读过一些评论说上面的代码是不安全的,但为什么它不安全?任何人都可以向我提供一个特定的例子,其中上面的代码导致错误?

另外,以下代码也是错误的.但为什么?擦除后似乎也能正常工作.

public class GenericArray<E>{
        E s= new E();
    }
Run Code Online (Sandbox Code Playgroud)

Mak*_*oto 7

数组声明需要具有可重新生成的类型,并且泛型不可恢复.

从文档中:您可以放置​​在数组上的唯一类型是可以重新生成的类型,即:

  • 它引用非泛型类或接口类型声明.

  • 它是一种参数化类型,其中所有类型参数都是无界通配符(第4.5.1节).

  • 它是原始类型(§4.8).

  • 它是一种原始类型(§4.2).

  • 它是一种数组类型(第10.1节),其元素类型是可恢复的.

  • 它是一种嵌套类型,其中,对于由"."分隔的每个类型T,T本身是可再生的.

这意味着"通用"数组的唯一合法声明就是这样List<?>[] elements = new ArrayList[10];.但这绝对不是通用数组,它是一个List未知类型的数组.

Java抱怨你执行强制转换E[]的主要原因是因为它是一个未经检查的强制转换.也就是说,您将从已检查的类型明确地转到未经检查的类型; 在这种情况下,检查的泛型类型E为未选中的类型Object.但是,这是创建通用数组的唯一方法,如果必须使用数组,通常认为是安全的.

通常,避免这种情况的建议是在可能的地方和时间使用通用集合.


dav*_*mac 3

这段代码似乎运行良好。为什么会导致编译时错误?

首先,因为它会违反类型安全(即它是不安全的- 见下文),并且一般来说,可以静态确定执行此操作的代码不允许编译。

请记住,由于类型擦除,类型E在运行时是未知的。该表达式new E[10]最多可以创建一个已擦除类型的数组,在本例中Object,呈现原始语句:

E[] s= new E[5];
Run Code Online (Sandbox Code Playgroud)

相当于:

E[] s= new Object[5];    
Run Code Online (Sandbox Code Playgroud)

这当然是不合法的。例如:

String[] s = new Object[10];
Run Code Online (Sandbox Code Playgroud)

...出于基本相同的原因,不可编译。

您认为删除后,该声明将是合法的,这意味着您认为这意味着原始声明也应被视为合法。然而这是不对的,可以通过另一个简单的例子来证明:

ArrayList<String> l = new ArrayList<Object>();
Run Code Online (Sandbox Code Playgroud)

删除上述内容将是ArrayList l = new ArrayList();,这是合法的,而原始内容显然是不合法的。

从更哲学的角度来看,类型擦除不应该​​改变代码的语义,但在这种情况下它会这样做 - 创建的数组将是一个数组Object而不是一个数组E(无论E可能是什么)。E然后可以在其中存储非对象引用,而如果该数组确实是一个E[],它应该生成一个ArrayStoreException.

为什么不安全?

E[] s= new E[5];(请记住,我们现在正在讨论已替换为 的情况E[] s = (E[]) new Object[5];)

它是不安全的(在本例中是type unsafe的缩写),因为它在运行时创建了一种情况,其中变量 ( s) 持有对对象实例的引用,该对象实例不是该变量声明类型的子类型(Object[]是不是 的子类型E[],除非E== Object)。

谁能给我提供一个具体的例子,其中上面的代码会导致错误?

本质问题是可以将非E对象放入通过执行强制转换创建的数组中(如 中所示(E[]) new Object[5])。例如,假设有一个foo带有Object[]参数的方法,定义为:

void foo(Object [] oa) {
    oa[0] = new Object();
}
Run Code Online (Sandbox Code Playgroud)

然后采取以下代码:

String [] sa = new String[5];
foo(sa);
String s = sa[0]; // If this line was reached, s would
                  // definitely refer to a String (though
                  // with the given definition of foo, this
                  // line won't be reached...)
Run Code Online (Sandbox Code Playgroud)

String即使在调用foo. 另一方面:

E[] ea = (E[]) new Object[5];
foo(ea);
E e = ea[0];  // e may now refer to a non-E object!
Run Code Online (Sandbox Code Playgroud)

该foo方法可能已将非E对象插入到数组中。因此,尽管第三行看起来安全,但第一行(不安全)却违反了保证安全的约束。

一个完整的例子:

class Foo<E>
{
    void foo(Object [] oa) {
        oa[0] = new Object();
    }

    public E get() {
        E[] ea = (E[]) new Object[5];
        foo(ea);
        return ea[0];  // returns the wrong type
    }
}

class Other
{
    public void callMe() {
        Foo<String> f = new Foo<>();
        String s = f.get();   // ClassCastException on *this* line
    }
}
Run Code Online (Sandbox Code Playgroud)

该代码在运行时会生成 ClassCastException,并且不安全。另一方面,没有不安全操作(例如强制转换)的代码不会产生此类错误。

另外,下面的代码也是错误的。但为什么?删除后似乎效果也很好。

有问题的代码:

public class GenericArray<E>{
    E s= new E();
}
Run Code Online (Sandbox Code Playgroud)

擦除后,这将是:

Object s = new Object();
Run Code Online (Sandbox Code Playgroud)

虽然这一行本身没问题,但将这些行视为相同会引入我上面描述的语义更改和安全问题,这就是编译器不会接受它的原因。举例说明为什么它可能会导致问题:

public <E> E getAnE() {
    return new E();
}
Run Code Online (Sandbox Code Playgroud)

...因为类型擦除后,“new E()”将变成“new Object()”,并且E从方法返回非对象显然违反了其类型约束(它应该返回 an E),因此是不安全的。如果上述方法可以编译,并且您使用以下命令调用它:

String s = <String>getAnE();
Run Code Online (Sandbox Code Playgroud)

...那么您将在运行时收到类型错误,因为您将尝试将 an 分配Object给String变量。

进一步说明/澄清:

  • 不安全(“类型不安全”的缩写)意味着它可能会导致原本正常的代码出现运行时类型错误。(它实际上意味着更多,但这个定义对于这个答案来说已经足够了)。
  • “安全”代码可能会导致一个ClassCastException或多个异常,但这些异常仅发生在明确定义的点。ArrayStoreException也就是说,通常只有ClassCastException在执行强制转换时才能获得 a,该操作本身就存在这种风险。ArrayStoreException同样,只有将值存储到数组中时才能获得 an 。
  • 编译器在抱怨操作不安全之前不会验证此类错误是否实际发生。它只知道某些操作可能会导致问题,并对这些情况发出警告。
  • 不能创建类型参数的新实例(或数组),这既是一种旨在保持安全性的语言功能,也可能是为了反映使用类型擦除所带来的实现限制。也就是说,new E()可能期望生成实际类型参数的实例,而实际上它只能生成已擦除类型的实例。允许它编译是不安全的并且可能会造成混乱。一般来说,您可以使用E它来代替实际类型,而不会产生任何不良影响,但实例化的情况并非如此。