为什么Java的Collection<E>.toArray()方法返回一个Object[]而不是一个E[]?

20
在Java泛型出现之前,Collection.toArray()无法知道开发人员期望的数组类型(特别是对于空集合)。据我所知,这是使用惯用语collection.toArray(new E[0])的主要理由。
使用泛型后,Collection<E>.toArray()只能返回一个由E及其特化实例组成的数组。我不明白为什么返回类型仍然是Object[]而不是E[]。我认为,返回E[]而不是Object[]不应该破坏现有代码。
参见:Collection.toArray()Collection.toArray(T[])和相关主题java: (String[])List.toArray() gives ClassCastException
3个回答

9
这是一个非常好的问题。答案是泛型也被称为"擦除"。这不仅仅是一个名字。由泛型编码的信息仅在编译时使用,然后被删除。因此,JVM甚至不知道这个泛型类型E,所以它不能创建数组E[]。
另一个方法toArray(T[] a)在运行时从参数中接收类型信息。这就是该方法原型为 T[] toArray(T[] a)的原因:它获取类型T的数组并可以返回类型T的数组。类型作为参数传递。

为什么相同的逻辑不能应用于Collection<E>.iterator()?http://download.oracle.com/javase/6/docs/api/java/util/Collection.html#iterator%28%29? - Bernhard Bodenstorfer
显然,通用数组的创建是问题所在。在我的通用类代码中,以下内容会引发编译器错误“通用数组创建”: final E[] returnArray = events.toArray(new E[events.size()]); - Bernhard Bodenstorfer
@Bernhard,你怎么创建new E[0]?这对JVM在运行时有什么意义?无论是Collection还是toArray()方法在运行时都没有关于E的任何信息。 - Lukas Eder
@Bernhard,迭代器不需要知道它返回的对象类型。相比之下,toArray()需要实际创建一个适当运行时类型的数组。 - Old Pro

5
"类型擦除"只是部分解释:无论是Collection还是它的toArray()方法,在运行时都没有关于E的任何信息。
这也是由于向后兼容性,Collection.toArray()仍必须返回Object[]。在Java 1.5之前,没有办法知道集合的泛型类型,因此这是唯一合理的API设计。

我同意你的观点,Lukas。此外,我本来想从向后兼容性问题开始,但后来决定擦除更为重要。实际上,擦除甚至不允许你返回类型化数组。因此,即使你从头开始创建Java并包括泛型,而且完全没有任何兼容性问题,你也无法做到这一点。 - AlexR
你说得对。但我不想重复你的答案 :-). 也可以检查一下 Collection.contains(Object)Collection.remove(Object),它们因兼容性原因而未被改动。 - Lukas Eder
1
不,它们并不是这样的。这是因为不同类的对象可以相等。 - newacct

1

@Lukas,关于:“new E[]”

新的E [0]引发了编译器错误,正如您可能预期的那样。我找到的解决方法是:

最终的E [] returnArray = (E [])events.toArray(new Event [events.size()]);

N.B. 代码在模板类Listener<E extends Event>中。

在我的解决方法中,类型擦除既是问题也是解决方案。对(E [])的转换是安全的,因为它的精确类型被擦除为Event []。我唯一看到的缺点是编译器警告“未经检查或不安全的操作”(显然,在这种情况下,由于类型擦除,强制转换并不是这种情况)。

@Lukas,关于向后兼容性

我认为向后兼容性没有大问题。使返回类型更特殊与使参数类型更特殊不同。

换句话说,迄今为止期望Collection.toArray()返回Object []的源代码应该很高兴收到E []。

至于字节码,由于类型擦除,Object []和E []本质上是相同的。


一些以上内容最好作为给定答案下的注释。new E[0] 应该可以工作。至于类型擦除,编译器在编译时会擦除泛型类型信息(请参见 https://dev59.com/MXRC5IYBdhLWcg3wUfN2)。因此,`toArray()` 方法在运行时没有访问类型信息,因此不能提供比 Object[] 更具体的运行时保证。另一方面,toArray(T[]) 在运行时获取传递的类型,并因此可以提供保证。 - Paul W
关于向后兼容性,请考虑以下代码:Collection<String> collection = (whatever); Object[] r = collection.toArray(); r[0] = new Object(); 在原始系统下是完全可接受的,但如果将 toArray 修改为返回 String[],则会导致运行时异常。(注意:数组的运行时类型不会被擦除,只有泛型类型会被擦除!) - Jules
另外,你忽略的编译器警告并不是无关紧要的。你的数组不是 E[],而是 Event[]。这意味着一旦你返回它,如果引用被复制到两个不同的代码部分,其中一个可能会执行以下操作:((Event[])returnedArray)[0] = new F();(其中 F 是扩展 Event 但不是 E 的类),这将导致在其他部分的泛型代码中的类型保证失败(该代码假定数组中的所有值都是 E 实例)。如果数组确实是 E[] 类型,则在尝试存储 F 时会引发适当的异常。 - Jules

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