假设我有一个数组列表

ArrayList<MyClass> myList;

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

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

over

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

?

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

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


当前回答

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

其他回答

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

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

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

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

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

第一种情况更有效。

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

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

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