忍受我一段时间.我知道这听起来有些主观和争论,但我发誓最后会有一个问号,这个问题实际上可以客观地回答......
来自.NET和C#背景,我近年来被语法糖所破坏,泛型与扩展方法相结合,在许多.NET解决方案中提供常见问题.使C#泛型非常强大的一个关键特性是,如果其他地方有足够的信息,编译器可以推断出类型参数,所以我几乎不必将它们写出来.在您意识到保存了多少次击键之前,您不必编写多行代码.例如,我可以写
var someStrings = new List<string>();
// fill the list with a couple of strings...
var asArray = someStrings.ToArray();
Run Code Online (Sandbox Code Playgroud)
和C#将只知道,我的意思是,第一var是List<string>,第二个是string[]那.ToArray()真的是.ToArray<string>().
然后我来到Java.
我已经对Java泛型有了足够的了解,知道它们是根本不同的,除此之外,编译器实际上并没有编译成通用代码 - 它剥离了类型参数并使其无论如何都以某种(非常复杂的)方式工作(我还没有真正了解).但即使我知道 Java中的泛型是根本不同的,我也无法理解为什么这些结构是必要的:
ArrayList<String> someStrings = new ArrayList<String>();
// fill the list with a couple of strings...
String[] asArray = someStrings.toArray(new String[0]); // <-- HERE!
Run Code Online (Sandbox Code Playgroud)
为什么我必须实例化一个新的String[],没有任何元素,不会用于任何东西,因为Java编译器知道它是什么String[]而不是我想要的任何其他类型的数组?
我意识到这是重载的方式,而且toArray()当前返回的是Object[].但是,当Java的这一部分被发明时,为什么要做出这个决定呢?为什么这个设计比跳过.toArray()那个Object[]完全返回重载并且只toArray()返回一个更好T[]?这是编译器的限制,还是框架这部分设计者的想象力,还是其他什么?
你可以从我对极其重要的东西的浓厚兴趣中得知,我有一段时间没有睡觉......
new*_*cct 12
不,这些原因大多是错误的.它与"向后兼容性"或类似的东西无关.这不是因为有一个返回类型的方法Object[](许多签名在适当的地方被更改为泛型).也不是因为获取数组会使其无法重新分配数组.他们并没有"错误地将其排除"或做出糟糕的设计决定.它们不包括a,T[] toArray()因为它不能用数组的工作方式和类型擦除在泛型中的工作方式编写.
声明List<T>签名的方法是完全合法的T[] toArray().但是,没有办法正确实现这样的方法.(你为什么不试试这项运动?)
请记住:
new Foo[x]或使用Array.newInstance()).因此,您无法创建类型参数组件类型的数组,即new T[...].
实际上,如果Lists有一个方法T[] toArray(),那么new T[n]当前不可能的泛型数组创建()是可能的:
List<T> temp = new ArrayList<T>();
for (int i = 0; i < n; i++)
temp.add(null);
T[] result = temp.toArray();
// equivalent to: T[] result = new T[n];
Run Code Online (Sandbox Code Playgroud)
泛型只是一个编译时的语法糖.可以通过更改一些声明并添加强制转换和填充来添加或删除泛型,而不会影响代码的实际实现逻辑.让我们比较1.4 API和1.5 API:
1.4 API:
Object[] toArray();
Object[] toArray(Object[] a);
Run Code Online (Sandbox Code Playgroud)
在这里,我们只有一个List对象.第一种方法具有所声明的返回类型Object[],和它创建运行时类的对象Object[].(请记住,编译时(静态)类型的变量和运行时(动态)类型的对象是不同的东西.)
在第二种方法中,假设我们创建一个String[]对象(即new String[0])并将其传递给它.数组具有基于其组件类型的子类型的子类型关系,因此String[]是子类Object[],因此这是查找.这里需要注意的最重要的是它返回运行时类的对象String[],即使它声明的返回类型是Object[].(再次,String[]是一个子类型Object[],所以这并不罕见.)
但是,如果您尝试将第一个方法的结果强制转换为类型String[],您将获得一个类强制转换异常,因为如前所述,它的实际运行时类型是Object[].如果你将第二种方法的结果(假设你传入了a String[])转换为String[],它将成功.
因此,即使您可能没有注意到它(两种方法似乎都返回Object[]),这两种方法之间的预泛化中的实际返回对象已经存在很大的根本区别.
1.5 API:
Object[] toArray();
T[] toArray(T[] a);
Run Code Online (Sandbox Code Playgroud)
该确切的同样的事情发生在这里.泛型添加了一些很好的东西,比如在编译时检查第二种方法的参数类型.但基本原理仍然相同:第一种方法创建一个实际运行时类型为的对象Object[]; 第二种方法创建一个对象,其实际运行时类型与传入的数组相同.
事实上,如果你试图传入一个数组,其类实际上是一个子类型T[],比如说U[],即使我们有一个List<T>,猜猜它会做什么?它会尝试将所有元素放入一个U[]数组(可能成功(如果所有元素都是类型U),或者失败(如果没有))返回一个实际类型为的对象U[].
所以早点回到我的观点.你为什么不能制定方法T[] toArray()?因为您不知道要创建的数组类型(使用new或Array.newInstance()).
T[] toArray() {
// what would you put here?
}
Run Code Online (Sandbox Code Playgroud)
为什么你不能只创建一个new Object[n]然后再将它投射到T[]?它不会立即崩溃(因为T在此方法中被删除),但是当你试图将它返回到外面时; 并假设外部代码请求一个特定的数组类型,例如String[] strings = myStringList.toArray();,它会抛出一个异常,因为有一个来自泛型的隐式转换.
人们可以尝试所有类型的黑客,比如查看列表的第一个元素来尝试确定组件类型,但这不起作用,因为(1)元素可以为null,(2)元素可以是子类型实际的组件类型,并且当您尝试将其他元素放入其中时,创建该类型的数组可能会失败.基本上,没有好办法解决这个问题.
这toArray(String[])部分是因为toArray在Java 1.5中引入泛型之前存在两种方法.那时候,没有办法推断出类型参数,因为它们根本就不存在.
Java在向后兼容性方面非常重要,因此这就是API的特定部分笨拙的原因.
整个类型擦除的东西也是为了保持向后兼容性.为1.4编译的代码可以愉快地与包含泛型的较新代码进行交互.
是的,它很笨拙,但至少它并没有打破引入泛型时存在的巨大Java代码库.
编辑:那么,作为参考,1.4 API是这样的:
Object[] toArray();
Object[] toArray(Object[] a);
Run Code Online (Sandbox Code Playgroud)
和1.5 API是这样的:
Object[] toArray();
T[] toArray(T[] a);
Run Code Online (Sandbox Code Playgroud)
我不确定为什么改变1-arg版本的签名而不是0-arg版本的签名.这似乎是一个合乎逻辑的变化,但也许有一些复杂性,我缺少.或许他们只是忘了.
EDIT2:在我看来,在这种情况下,Java 应该使用推断类型参数(如果可用),以及显式类型,其中推断类型不可用.但我怀疑实际包含在语言中会很棘手.
也有人回答"为什么"的问题(我喜欢卡梅隆Skinner的答案),我只想补充一点,你不会有每次实例化一个新的阵列,它并不一定是空的.如果数组足够大以容纳集合,则它将用作返回值.从而:
String[] asArray = someStrings.toArray(new String[someStrings.size()])
将只分配一个正确大小的单个数组,并使用Collection中的元素填充它.
此外,一些Java集合实用程序库包括静态定义的空数组,可以安全地用于此目的.例如,请参阅Apache Commons ArrayUtils.
编辑:
在上面的代码中,当Collection为空时,实例化的数组实际上是垃圾.由于数组不能在Java中调整大小,因此空数组可以是单例.因此,使用库中的常量空数组可能稍微更有效.
这是因为类型擦除.请参阅维基百科关于Java泛型的文章中的类型擦除问题:泛型类型信息仅在编译时可用,它完全被编译器剥离并且在运行时不存在.
因此toArray需要另一种方法来确定要返回的数组类型.
维基百科文章中提供的示例非常具有说明性:
ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) // evaluates to true <<==
System.out.println("Equal");
Run Code Online (Sandbox Code Playgroud)