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

17

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

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

3
请问您是否已经阅读了我的帖子?您转发给我的问题的答案实际上并没有解决我的问题。请问您是否已经阅读了我的帖子? - Zhu Li
1
@MarounMaroun 这个问题更多地涉及到编译器为什么会抱怨泛型数组,而你提供的链接是关于如何创建它们的。我想这不是一个重复的问题。 - Tom
4
我重新打开了这个问题,感谢您的澄清。 - Maroun
对于 E[] s= (E[])new Object[5];,你尝试过 String[] arr = new GenericArray<String>().s; 吗? - Radiodef
1
@Radiodef 我已经尝试过了,现在我明白为什么这是不安全的。谢谢。 - Zhu Li
4个回答

7

数组声明需要具有可重用类型, 而 泛型不是可重用的。

根据文档:您可以放置在数组上的唯一类型是可重用的,即:

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

  • 它是一个参数化类型,其中所有类型参数都是无限制通配符 (§4.5.1)。

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

  • 它是一个基本类型 (§4.2)。

  • 它是一个元素类型可重用的数组类型 (§10.1)。

  • 它是一个嵌套类型,在每个由 "." 分隔的类型 T 中,T 本身是可重用的。

这意味着“通用”数组的唯一合法声明将类似于List<?>[] elements = new ArrayList[10];。但这绝对不是通用数组,它是未知类型List的数组。
Java抱怨你执行E[]转换的主要原因是因为它是未经检查的转换。也就是说,你从一个已检查的类型显式地转换为一个未经检查的类型;在这种情况下,从已检查的通用类型E到未经检查的类型Object。然而,这是创建通用数组的唯一方法,如果必须使用数组,则通常被认为是安全的。
总的来说,避免这种情况的建议是在可以使用通用集合的地方尽可能使用它们。

你的意思是说 public class GenericArray<E>{ E[] s= (E[])new Object[5]; } 的潜在问题只是可能会导致运行时 ClassCastException 吗? - Zhu Li
可能会。你告诉语言内置的类型检查器,你知道自己在做什么,并且只会将类型为E的元素插入到那个Object[]中。这个操作通常对于数组是安全的,但Java仍然会引发一个未经检查的警告,让你知道它无法安全地检查对象进入数组的类型。 - Makoto
我现在明白了。由于类型擦除,转型检查实际上在这里并不起作用。这就是为什么它是不安全的。谢谢。 - Zhu Li

3
这段代码片段看起来很好用。为什么它会导致编译时错误?
首先,因为它会违反类型安全性(即它是不安全的-见下文),通常静态确定会这样做的代码是不允许编译的。
请记住,由于类型擦除,类型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];的情况。)
这是不安全的(在这种情况下是指类型不安全),因为它在运行时创建了一种情况,即一个变量(s)持有对一个对象实例的引用,而该对象实例不是变量声明类型的子类型(Object[]不是E[]的子类型,除非E == Object)。
“能否提供一个特定的例子,说明上述代码会导致错误?”
基本问题在于,通过执行强制类型转换(如(E[]) new Object[5])创建的数组中可以放置非E对象。例如,假设有一个接受Object[]参数的方法foo,定义为:
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,而在数组中存储值时才会出现ArrayStoreException
  • 编译器并不验证这种错误是否真的会在运行时发生,它只知道某些操作潜在地可能会导致问题,并警告这些情况。
  • 不能创建一个类型参数的新实例(或数组)既是一种设计用于保持安全性的语言特性,也可能反映了使用类型擦除所带来的实现限制。也就是说,new E()可能被期望产生实际类型参数的实例,而实际上它只能产生擦除类型的实例。允许它编译将是不安全和可能令人困惑的。通常你可以在实际类型的位置使用E而不会有任何负面影响,但在实例化时情况并非如此。

@ZhuLi,你的示例使用了强制类型转换。正如我在上面所述,没有强制类型转换的代码应该是安全的,这就是为什么E[] ea = new E[5]不被允许的原因。是的,错误会在运行时被发现,但错误将是一个ClassCastException,并且会出现在不包含强制类型转换的行上,可能在另一个(完全“无辜”的)类中。由于类型擦除(请参见我上面的其他评论中给出的链接),无法实例化确切的参数类型。 - davmac
@ZhuLi "...而后者似乎没什么问题"——但我的回答已经解释了为什么不行。我已修改答案,展示一个完整的代码示例。是的,强制类型转换可以被用于非常规的方式(通常也会被使用),但它有潜在的问题(正如我的答案所示),特别是它违反了“金科玉律”——没有强制类型转换的代码都是安全的。因此,它是不安全的。 - davmac
公共类Foo { void foo(Object [] oa) { oa[0] = new Object(); } public String get() { String[] ea = new String[5]; foo(ea); return ea[0]; // 返回了错误的类型 } public static void main(String[] args){ new Other().callMe(); } } class Other { public void callMe() { Foo f = new Foo(); String s = f.get();
}
} 我对此进行了修改,但仍然导致运行时异常。我的意思是在这里导致异常的关键不是E[] ea = (E[]) new Object[5];
- Zhu Li
我的意思是,导致异常的关键不是E[] ea = (E[]) new Object[5];,而是棘手的方法foo(Object[])。虽然有所不同,但运行时异常变成了ArrayStoreException。 - Zhu Li
我想要展示的是,无论是否使用E[] ea = (E[]) new Object[5]这种方法,都会导致运行时异常。因此,你的例子实际上告诉我们要避免使用那种方法,而不是所谓的泛型数组。如果你的例子意味着我们应该避免使用E[] ea = (E[]) new Object[5],那么它也意味着我们在某种程度上不应该使用String[] ea = new String[5];,因为它也无法防止错误发生。 - Zhu Li
显示剩余8条评论

2
一个编译器可以使用类型为Object的变量来执行类型为Cat的变量所能执行的任何操作。编译器可能需要添加一个类型转换,但这种类型转换要么会抛出异常,要么会产生对Cat实例的引用。因此,SomeCollection<T>的生成代码不必实际使用任何类型为T的变量;编译器可以将T替换为Object,并在必要时将函数返回值之类的内容强制转换为T
然而,编译器不能使用Object[]来执行Cat[]所能执行的所有操作。如果SomeCollection[]具有类型为T[]的数组,则无法创建该数组类型的实例,除非知道T的类型。它可以创建一个Object[]的实例,并在其中存储对T实例的引用,而不知道T的类型,但任何尝试将这样的数组转换为T[]都保证会失败,除非T恰好是Object

但是 Object[] 可以转换为 T[]。 - Zhu Li
@ZhuLi:如果一个类型为Object[]的变量标识了一个类型为T[]的实例,那么它可以成功地转换。然而,如果一个类型为Object的变量标识了一个Object[]的实例,转换将在运行时失败。 - supercat
@ZhuLi:如果Java Runtime除了primitiveArrayreferenceArray之外,消除了所有数组类型以及所有比int短的原始类型,并且编译器根据其编译时类型生成适当的指令来加载/存储数组中的内容,那么它本可以更简化。因此,类型为Cat []的变量将保存对referenceArray对象的引用,并且每次读取元素时,系统都会确认它是否真的是一个Cat,否则会抛出异常。如果这样做了,那么泛型数组就不会成为问题。但事实并非如此... - supercat
数组具有更具体的类型[这个事实允许从数组中读取数据而无需进行类型检查,但在写入时需要进行类型检查]。无论如何,通常可以通过使用类型为“Object”的数组来处理泛型的情况。 - supercat
我想我更好地理解了这个问题。谢谢。 - Zhu Li

0
假设Java允许通用数组。现在,请看下面的代码:
Object[] myStrs = new Object[2];
myStrs[0] = 100;  // This is fine
myStrs[1] = "hi"; // Ambiguity! Hence Error.

如果用户被允许创建通用数组,那么用户可以像上面的代码一样做,这会使编译器混淆。这违背了数组的目的(数组只能处理相同/类似/同质类型的元素,记住吗?)。如果您想要异构数组,您总是可以使用类/结构体的数组。

更多信息在这里


4
你的例子对我来说没有意义。你在例子中看到了哪里有一个“通用”的数组?由于编译器能够检查使用通用类型的集合的正确性,因此它也能够为通用数组执行相同的操作。 - Tom
由于Object是JAVA中所有类的超类,我将Object作为数组的数据类型,间接地暗示了泛型数组。你可以在关于它的问题中看到Object[] s = new Object[5];这个语句。此外,我想给出一个简单的例子来说明为什么这是不可能的。 :) - Abhishek
3
我将数组的数据类型设置为Object,这间接地暗示了泛型数组。然而,泛型数组仅接受其泛型类型及其子类型,而 Object 则接受任何类型。它们并没有太多共同之处。在问题中提到的 Object[] s= new Object[5]; 是在类型擦除后和编译器检查了你答案中所描述的错误后才出现的。你的例子仍然没有意义。 - Tom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接