假设我有一个数组列表

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'的任何优化。

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

其他回答

从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;
}

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

与直觉相反,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编译器包含一些优化,这些优化使它能够廉价地创建和初始化一个新的大小正确的数组,如果您自己创建数组,则不能使用这些优化。

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

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

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

来自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).

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

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

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