繁体   English   中英

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

[英]Why can't I create an array of a type parameter in Java?

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

public class GenericArray<E>{
    E[] s= new E[5];
}

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

public class GenericArray{
    Object[] s= new Object[5];
}

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

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

public class GenericArray<E>{
    E[] s= (E[])new Object[5];
}

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

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

public class GenericArray<E>{
        E s= new E();
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

E[] s= new E[5];

相当于:

E[] s= new Object[5];    

这当然不合法。 例如:

String[] s = new Object[10];

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

您认为在删除后,该陈述将是合法的,暗示您认为这意味着原始陈述也应被视为合法。 然而,这是不对的,正如另一个简单的例子所示:

ArrayList<String> l = new ArrayList<Object>();

上面的擦除将是ArrayList l = new ArrayList(); ,这是合法的,而原来显然不是。

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

为什么不安全?

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

它是不安全的(在这种情况下是短期的类型不安全的 ),因为它在运行时创建其中一个变量(这种情况s )持有一个对象实例的引用,这是不是一个子类型变量的声明的类型( Object[]不是E[]的子类型,除非E == Object )。

任何人都可以向我提供一个特定的例子,其中上面的代码导致错误?

基本问题是可以将非E对象放入您通过执行转换创建的数组中(如(E[]) new Object[5] )。 例如,假设有一个方法foo ,它接受一个Object[]参数,定义如下:

void foo(Object [] oa) {
    oa[0] = new Object();
}

然后采取以下代码:

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...)

即使在调用foo之后,该数组肯定包含String对象。 另一方面:

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

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

代码在运行时会生成ClassCastException,并且不安全。 另一方面,没有诸如强制转换之类的不安全操作的代码不能产生这种类型的错误。

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

有问题的代码:

public class GenericArray<E>{
    E s= new E();
}

擦除后,这将是:

Object s = new Object();

虽然这条线本身很好,但将线条视为相同会引入我上面描述的语义变化和安全问题,这就是编译器不接受它的原因。 作为它可能导致问题的原因的一个例子:

public <E> E getAnE() {
    return new E();
}

...因为在类型擦除之后,'new E()'将成为'new Object()'并且从该方法返回非E对象明显违反其类型约束(它应该返回E )因此是不安全的 如果上面的方法是编译的,你用它调用它:

String s = <String>getAnE();

...然后你会在运行时得到一个类型错误,因为你试图将一个Object分配给一个String变量。

进一步说明/澄清:

  • 不安全 (这是“类型不安全”的缩写)意味着它可能会导致代码中的运行时类型错误,否则将是合理的。 (它实际上意味着更多,但这个定义足以满足这个答案的目的)。
  • 使用“安全”代码可能会导致ClassCastExceptionArrayStoreException或其他异常,但这些异常仅发生在明确定义的点上。 也就是说,在执行ClassCastException时,通常只能获得ClassCastException ,这种操作本身就存在这种风险。 同样,在将值存储到数组中时,只能获得ArrayStoreException
  • 在抱怨操作不安全之前,编译器不会验证是否会发生此类错误。 它只知道某些操作可能会导致问题,并警告这些情况。
  • 您无法创建类型参数的新实例(或数组)既是为保护安全而设计的语言功能,也可能是为了反映使用类型擦除所带来的实现限制。 也就是说,可能期望new E()生成实际类型参数的实例,而实际上它只能生成擦除类型的实例。 允许它编译将是不安全的并且可能令人困惑。 通常,您可以使用E代替实际类型而不会产生不良影响,但实例化不是这种情况。

编译器可以使用Object类型的变量来执行Cat类型的变量可以执行的任何操作。 编译器可能必须添加类型转换,但是这样的类型转换将抛出异常或产生对Cat实例的引用。 因此,为SomeCollection<T>生成的代码不必实际使用任何类型为T变量; 编译器可以用Object替换T ,并在必要时将函数返回值等函数转换为T

但是,编译器不能使用Object[]来执行Cat[]可以执行的所有操作。 如果SomeCollection[]有一个类型为T[]的数组,则在不知道T类型的情况下,它将无法创建该数组类型的实例。 它可以创建一个Object[]实例并在不知道T类型的情况下存储对T实例的引用,但是除非T碰巧是Object否则任何将这样的数组转换为T[]尝试都将保证失败。

假设Java中允许使用通用数组。 现在,看看下面的代码,

Object[] myStrs = new Object[2];
myStrs[0] = 100;  // This is fine
myStrs[1] = "hi"; // Ambiguity! Hence Error.

如果允许用户创建通用数组,那么用户可以按照我在上面的代码中所示进行操作,这会使编译器混淆。 它违背了数组的目的( Arrays can handle only same/similar/homogeneous type of elements ,还记得吗?)。 如果需要异构数组,可以始终使用class / struct数组。

更多信息在这里

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM