使用 .toArray(new MyClass[0]) 还是 .toArray(new MyClass[myList.size()])?

219

假设我有一个ArrayList

ArrayList<MyClass> myList;

我想调用toArray方法,是否有性能上的原因使用它?

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

结束

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

我喜欢第二种方式,因为它不那么冗长,并且我假设编译器会确保空数组并没有真正被创建,但我一直在想这是否属实。

当然,在99%的情况下,这两种方式都没有区别,但我想在我的普通代码和优化后的内部循环之间保持一致的风格......


10
看起来问题现在已经在Aleksey Shipilёv的新博客文章《古代智慧的数组》中得到了解决! - glts
9
从博客文章中:「总之,toArray(new T[0]) 似乎更快、更安全、更规范化,因此现在应该是默认选择。」 - DavidS
8个回答

159

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

3
非常有趣的评论。我很惊讶没有人对此发表评论。我猜这是因为它与其他答案在速度方面存在矛盾。值得注意的是,这个人的声誉几乎比所有其他答案的声誉加起来还要高。 - Pimp Trizkit
2
@PimpTrizkit 刚刚检查了一下:使用额外变量与预期无异,使用流所需时间比直接调用 toArray 多60%至100%(大小越小,相对开销越大)。 - assylias
3
这个结论也在这里被发现:http://shipilev.net/blog/2016/arrays-wisdom-ancients/ - user167019
1
@xenoterracide 如上面评论中所讨论的,流式处理速度较慢。 - assylias
1
我不明白为什么IntelliJ建议我用零大小的数组替换我的预定大小的数组。谢谢...现在我知道了!我可以看到@АнтонАнтонов在下面提到了它。 - kevinarpe
显示剩余3条评论

122
截至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;
}

使用第一种方法,这样只创建一个对象并避免(隐式但仍然昂贵的)强制转换。

1
以下是关于编程的内容,可能对某些人有所帮助:1) LinkedList.toArray(T[] a) 的速度更慢(使用反射:Array.newInstance)且更复杂;2) 另一方面,在JDK7版本中,我非常惊讶地发现,通常效率极低的Array.newInstance执行速度几乎与普通的数组创建相同! - java.is.for.desktop
1
@ktaria的size是ArrayList的私有成员,指定了列表中的元素数量。请参阅ArrayList源代码 - MyPasswordIsLasercats
3
如果没有基准测试,猜测性能只适用于微不足道的情况。实际上,new Myclass[0]更快:https://shipilev.net/blog/2016/arrays-wisdom-ancients/ - Karol S
2
自JDK6+起,此答案已不再有效。 - Антон Антонов

45
JetBrains IntelliJ IDEA inspection中:

有两种方式将集合转换为数组: 1. 预设大小的数组,例如 c.toArray(new String[c.size()]) 2. 空数组,例如 c.toArray(new String[0]) 在旧版本的Java中,推荐使用预设大小的数组,因为创建正确大小的数组需要使用反射调用,速度较慢。
然而,自从OpenJDK 6的最新更新以来,这个调用已经被内部化,使得空数组版本的性能与预设大小版本相同,甚至有时更好。此外,对于并发或同步的集合,传递预设大小的数组是危险的,因为在 sizetoArray 调用之间可能存在数据竞争。如果在操作期间集合被同时缩小,可能会在数组末尾产生额外的 null 值。
请使用检查选项来选择首选的方式。

3
如果这些都是复制/引用的文本,我们能否相应地进行格式化,并提供源链接?我实际上是因为IntelliJ检查而来到这里的,我非常想查看所有检查和背后的原因的链接。 - Tim Büthe
4
您可以在此处查看检查文本:https://github.com/JetBrains/intellij-community/tree/master/plugins/InspectionGadgets/src/inspectionDescriptions - Антон Антонов
https://github.com/JetBrains/intellij-community/blob/master/plugins/InspectionGadgets/src/inspectionDescriptions/ToArrayCallWithZeroLengthArrayArgument.html - Petrakeas

18

现代的JVM在反射数组构造方面进行了优化,因此性能差异非常小。在这种样板代码中两次命名集合并不是一个好主意,因此我会避免使用第一种方法。第二种方法的另一个优点是它可以与同步和并发集合一起使用。如果想要进行优化,可以重用空数组(空数组是不可变的,可以共享),或使用分析工具(!)。


2
点赞“重复使用空数组”,因为它是可读性和潜在性能之间的折中方案,值得考虑。传递一个声明为private static final MyClass [] EMPTY_MY_CLASS_ARRAY = new MyClass [0]的参数并不能防止通过反射构造返回的数组,但它确实可以防止每次都构造一个额外的数组。 - Michael Scheper
Machael是正确的,如果您使用零长度数组,那么没有其他方法:(T [])java.lang.reflect.Array.newInstance(a.getClass()。getComponentType(),size); 如果大小大于或等于actualSize(JDK7),则这是多余的。 - Alex
如果您能提供“现代JVM在这种情况下优化反射数组构造”的引用,我将很高兴地为此答案点赞。 - Tom Panning
我正在学习。如果我使用以下代码: MyClass[] arr = myList.stream().toArray(MyClass[]::new); 这会对同步和并发集合有帮助还是有害?为什么?谢谢。 - Pimp Trizkit
1
当您在同步集合上调用.stream().toArray(MyClass[]::new)时,您会失去同步,并且必须手动同步。在并发集合的情况下,这并不重要,因为两种toArray方法都是弱一致的。无论哪种情况,直接在集合上调用toArray(new MyClass[0])可能会更快。 (考虑到问题后引入的API,即JDK 11+,直接在集合上调用.toArray(MyClass[]::new)只是委托给.toArray(new MyClass[0]),因为那已经是最好的方法了。) - Holger

3

toArray检查传递的数组是否具有正确的大小(即足够大以容纳列表中的元素),如果是,则使用该数组。因此,如果提供的数组大小小于所需大小,则会自反地创建一个新数组。

在您的情况下,大小为零的数组是不可变的,因此可以安全地将其提升为静态final变量,这可能使您的代码更清晰,避免在每次调用时创建数组。无论如何,在方法内部都将创建一个新数组,因此这是一种可读性优化。

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


2
第一种情况更有效率。这是因为在第二种情况下:
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() 不使用反射。无论如何,只要不将“强制转换”算作反射。;-) - Georgi
toArray(T[])方法需要创建一个适当类型的数组。现代JVM会优化这种反射方式,使其速度与非反射版本大致相同。 - Tom Hawtin - tackline
我认为它确实使用了反射。JDK 1.5.0_10肯定使用了反射,而且反射是我知道的唯一创建不在编译时期确定类型数组的方法。 - Panagiotis Korros
然后,其中一个源代码示例(上面的或我的)已过时。不幸的是,我没有找到正确的子版本号码。 - Georgi
1
Georgi,你的代码来自JDK 1.6,如果你查看Arrays.copyTo方法的实现,你会发现它使用了反射。 - Panagiotis Korros
在现代JVM(至少Java 8)中,情况已经不再如此。第二个选项更快,正如这里最受欢迎的答案所断言的那样。 - Clint Eastwood

0

第二种方法稍微更易读,但改进很小,不值得。第一种方法更快,运行时没有缺点,所以我使用它。但我写成第二种方式,因为打字更快。然后我的IDE会标记它并提供修复。只需按下一个按键,它就会将代码从第二种类型转换为第一种。


-3

若使用正确大小的数组与'toArray'一起,性能将更佳,因为替代方案会先创建零大小的数组,然后创建正确大小的数组。然而,正如您所说,差异可能微不足道。

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

那么,对于您的问题,主要是风格问题,但出于一致性考虑,应该成为您遵循的任何编码标准的一部分(无论是否记录)。


另一方面,如果标准是使用零长度数组,则偏离该标准的情况意味着性能是一个问题。 - Michael Scheper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接