假设我有一个数组列表
ArrayList<MyClass> myList;
我想调用toArray,是否有性能方面的原因
MyClass[] arr = myList.toArray(new MyClass[myList.size()]);
over
MyClass[] arr = myList.toArray(new MyClass[0]);
?
我更喜欢第二种风格,因为它不那么冗长,而且我假设编译器将确保不会真正创建空数组,但我一直在想这是否属实。
当然,在99%的情况下,它不会以某种方式或其他方式产生区别,但我希望在我的普通代码和优化的内部循环之间保持一致的风格……
第一种情况更有效。
这是因为在第二种情况下:
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;
}
通过使用第一种形式,可以避免创建第二个数组,也可以避免反射代码。
从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编译器包含一些优化,这些优化使它能够廉价地创建和初始化一个新的大小正确的数组,如果您自己创建数组,则不能使用这些优化。
来自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).