假设我有一个数组列表

ArrayList<MyClass> myList;

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

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

over

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

?

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

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


使用'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中的ArrayList开始,如果数组的大小合适(或更大),它就会被填充。因此

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的检测:

There are two styles to convert a collection to an array: either using a pre-sized array (like c.toArray(new String[c.size()])) or using an empty array (like c.toArray(new String[0]). In older Java versions using pre-sized array was recommended, as the reflection call which is necessary to create an array of proper size was quite slow. However since late updates of OpenJDK 6 this call was intrinsified, making the performance of the empty array version the same and sometimes even better, compared to the pre-sized version. Also passing pre-sized array is dangerous for a concurrent or synchronized collection as a data race is possible between the size and toArray call which may result in extra nulls at the end of the array, if the collection was concurrently shrunk during the operation. This inspection allows to follow the uniform style: either using an empty array (which is recommended in modern Java) or using a pre-sized array (which might be faster in older Java versions or non-HotSpot based JVMs).