.toArray(新的MyClass[0])或.toArray(新的MyClass[myList.size()])?

假设我有一个数组列表

ArrayList<MyClass> myList;

我想调用toArray,是否有性能方面的原因

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

MyClass[] arr = myList.toArray(new MyClass[0]);

?

我更喜欢第二种风格,因为它不那么冗长,而且我假设编译器将确保不会真正创建空数组,但我一直在想这是否属实。

当然,在99%的情况下,它不会以某种方式或其他方式产生区别,但我希望在我的普通代码和优化的内部循环之间保持一致的风格……

90172 次浏览

使用'toArray'和正确大小的数组将执行得更好,因为替代方法将首先创建零大小的数组,然后创建正确大小的数组。然而,正如你所说,这种差异可能是可以忽略不计的。

另外,请注意javac编译器不执行任何优化。现在所有的优化都是由JIT/HotSpot编译器在运行时执行的。我不知道任何jvm中围绕'toArray'的任何优化。

那么,问题的答案很大程度上是一个风格问题,但为了一致性,应该成为您所遵循的任何编码标准的一部分(无论是有文档的还是其他的)。

第一种情况更有效。

这是因为在第二种情况下:

MyClass[] arr = myList.toArray(new MyClass[0]);

运行时实际上创建了一个空数组(大小为零),然后在toArray方法中创建另一个数组以适应实际数据。这个创建是使用下面的代码(来自jdk1.5.0_10)通过反射完成的:

public <T> T[] toArray(T[] a) {
if (a.length < size)
a = (T[])java.lang.reflect.Array.
newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

通过使用第一种形式,可以避免创建第二个数组,也可以避免反射代码。

toArray检查传递的数组大小是否正确(也就是说,大到足以容纳列表中的元素),如果是,则使用该数组。因此,如果数组的大小小于所需的大小,则会反射性地创建一个新数组。

在您的例子中,大小为0的数组是不可变的,因此可以安全地提升为静态final变量,这可能会使您的代码更简洁,从而避免在每次调用时创建数组。无论如何,方法内部都会创建一个新数组,因此这是一个可读性优化。

可以说,更快的版本是传递正确大小的数组,但除非你可以证明此代码是一个性能瓶颈,优先考虑可读性而不是运行时性能,直到证明不是这样。

Java 5中的数组列表开始,如果数组的大小合适(或更大),则数组已经被填充。因此

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

将创建一个数组对象,填充它并返回给"arr"。另一方面

MyClass[] arr = myList.toArray(new MyClass[0]);

将创建两个数组。第二个是长度为0的MyClass数组。这里有一个对象创建用于一个马上会被丢弃的对象。就源代码所示,编译器/ JIT无法优化这个对象,因此不会创建它。此外,使用零长度对象会导致在toArray() -方法中进行强制转换。

请参阅ArrayList.toArray()的源代码:

public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

使用第一种方法只创建一个对象,并避免(隐式但代价昂贵的)强制类型转换。

在这种情况下,现代jvm优化了反射数组结构,因此性能差异很小。在这样的样板代码中两次命名集合并不是一个好主意,因此我将避免使用第一个方法。第二种方法的另一个优点是它与同步和并发收集一起工作。如果您想进行优化,可以重用空数组(空数组是不可变的,可以共享),或者使用分析器(!)。

第二个版本可读性稍好,但改进甚微,不值得这么做。第一种方法更快,在运行时没有缺点,所以我就用它。但我用第二种方式写,因为这样打字更快。然后我的IDE将其标记为警告并提供修复。只需击一次键,它就能将第二种类型的代码转换为第一种类型的代码。

与直觉相反,Hotspot 8上最快的版本是:

MyClass[] arr = myList.toArray(new MyClass[0]);

我已经使用jmh运行了一个微基准测试,结果和代码如下所示,显示具有空数组的版本始终优于具有预大小数组的版本。注意,如果可以重用大小正确的现有数组,结果可能会不同。

基准测试结果(以微秒为单位,越小=越好):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025 ▒ 0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155 ▒ 0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512 ▒ 0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884 ▒ 0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147 ▒ 0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977 ▒ 5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019 ▒ 0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133 ▒ 0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075 ▒ 0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318 ▒ 0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652 ▒ 0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692 ▒ 8.957  us/op

作为参考,代码如下:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
@Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
private final List<Integer> list = new ArrayList<>();
@Setup public void populateList() {
for (int i = 0; i < n; i++) list.add(0);
}
@Benchmark public Integer[] preSize() {
return list.toArray(new Integer[n]);
}
@Benchmark public Integer[] resize() {
return list.toArray(new Integer[0]);
}
}

你可以在博客文章古人的智慧阵列中找到类似的结果,完整的分析和讨论。总结一下:JVM和JIT编译器包含一些优化,这些优化使它能够廉价地创建和初始化一个新的大小正确的数组,如果您自己创建数组,则不能使用这些优化。

来自JetBrains Intellij Idea的检测:

将集合转换为数组有两种样式:using 一个预先大小的数组(如c.toArray(新的字符串[c.size ())))或 使用空数组(如c.toArray(新字符串[0])。< p > 推荐使用预大小数组的旧Java版本,如 反射调用,这是创建适当大小的数组所必需的 非常慢。但是由于OpenJDK 6的后期更新,这个调用 被内嵌,使得性能的空数组版本 与预尺寸相比,效果相同,有时甚至更好 的版本。同时,传递预大小数组对于并发或 同步收集作为一种数据竞赛是可能的 大小toArray调用,可能导致额外的空值 如果集合被并发收缩,则在数组的末尾 在手术过程中。

此检查允许遵循 统一样式:使用空数组(在 现代Java)或使用预大小的数组(在 旧的Java版本或非基于hotspot的jvm)。< / p > < / p >