【问题标题】:Collection.toArray() vs Collection.stream().toArray()Collection.toArray() 与 Collection.stream().toArray()
【发布时间】:2017-02-02 21:19:05
【问题描述】:

考虑以下代码:

List<String> myList = Arrays.asList(1, 2, 3);
String[] myArray1 = myList.toArray(new String[myList.size()]);
String[] myArray2 = myList.stream().toArray(String[]::new);
assert Arrays.equals(myArray1, myArray2);

在我看来,使用流要简单得多。

因此,我测试了每一个的速度。

List<String> myList = Arrays.asList("1", "2", "3");
double start;

start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
    String[] myArray1 = myList.toArray(new String[myList.size()]);
    assert myArray1.length == 3;
}
System.out.println(System.currentTimeMillis() - start);

start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
    String[] myArray2 = myList.stream().toArray(String[]::new);
    assert myArray2.length == 3;
}
System.out.println(System.currentTimeMillis() - start);

结果是使用流大约慢了四倍。在我的机器上,816 毫秒(流)与 187 毫秒(无流)。我还尝试切换时间语句(myArray2 在 myArray1 之前),这对结果影响不大。为什么这么慢?创建Stream 的计算量这么大吗?

我听从了@Holger 的建议,对 JVM 测试进行了一些研究(当然还不够),阅读了this postthis articlethis article,并使用了JMH


结果(通过 JMH):

private static final List<String> myList = IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList());

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.stream().toArray(String[]::new);
}

StreamToArrayArrayListBenchmark.testMethod avgt 5 2846.346 ± 32.500 ns/op

private static final List<String> myList = IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList());

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.toArray(new String[0]);
}

ToArrayEmptyArrayListBenchmark.testMethod avgt 5 1417.474 ± 20.725 ns/op

private static final List<String> myList = IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList());

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.toArray(new String[myList.size()]);
}

ToArraySizedArrayListBenchmark.testMethod avgt 5 1853.622 ± 178.351 ns/op


private static final List<String> myList = new LinkedList<>(IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList()));

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.stream().toArray(String[]::new);
}

StreamToArrayLinkedListBenchmark.testMethod avgt 5 4152.003 ± 59.281 ns/op

private static final List<String> myList = new LinkedList<>(IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList()));

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.toArray(new String[0]);
}

ToArrayEmptyLinkedListBenchmark.testMethod avgt 5 4089.550 ± 29.880 ns/op

private static final List<String> myList = new LinkedList<>(IntStream.range(1, 1000).mapToObj(String::valueOf).collect(Collectors.toList()));

@Benchmark
public void testMethod() {
    String[] myArray = myArrayList.toArray(new String[myList.size()]);
}

ToArraySizedArrayListBenchmark.testMethod avgt 5 4115.557 ± 93.964 ns/op


总结一下:

              | ArrayList | LinkedList
stream        | 2846      | 4152
toArray sized | 1853      | 4115
toArray empty | 1417      | 4089

使用 JMH(可能很天真),我仍然看到 ArrayList::toArray 的速度大约是 Stream::toArray 的两倍。然而,这似乎是因为ArrayList 能够只进行数组复制,正如@Andreas 指出的那样,因为当源是LinkedList 时,结果大致相同。

很高兴知道myList.toArray(new String[0])

【问题讨论】:

  • 如果你使用Arrays.stream()会怎样?
  • @JoshLee 在我的真实代码中,我有一个要转换为数组的列表。我不是我会如何使用 Arrays.stream() 吗?
  • @Holger 你是说把它标记为重复我的结论是错误的吗?我并不是真的在问如何进行基准测试——我假设我的基准测试不是 100% 准确的。我真的在问Stream 的开销——另一个问题没有涉及。
  • 我们不是在谈论“100% 准确”,您违反了那里提到的每条 规则。当然,从这种根本上被打破的基准得出的结论是错误的。对于现实生活中的用例,这些方法之间的实际差异接近于零。
  • @Holger 好的——这是对我的问题的合理回答。我仍然不明白这是如何重复的。如果你的答案是“你的基准测试是错误的——这些是等价的”,为什么不在答案中这么说呢?

标签: java collections java-stream


【解决方案1】:

Arrays.asList() 创建一个固定大小的List,它直接由可变参数数组参数支持。 Javadoc 甚至这样说:

返回由指定数组支持的固定大小的列表。

toArray() 的实现是一个简单的System.arraycopy()非常快

另一方面,当您执行myList.stream().toArray(String[]::new) 时,大小是未知的,因此Stream.toArray() 方法必须消耗流,收集所有值,然后创建数组并将值复制到数组中。这会慢很多,并且需要很多更多的内存

总之就是浪费资源。

如果你想要更简单,就不要给出数组大小。它仍然比使用 Streams 更快且占用更少的内存:

String[] myArray1 = myList.toArray(new String[0]);

【讨论】:

  • 我不知道您可以将 toArray 与空数组一起使用。谢谢你的提示!给它一个预先确定大小的数组似乎在速度方面几乎是一样的。
  • 我刚刚尝试使用 LinkedList 而不是 Arrays.asList() 并且行为相同,并且 LinkedList 与 ArrayList 的时间几乎相同。似乎所有开销都在 Stream 中。
  • 无论你如何切片(可以这么说),数组复制只是对 JVM 的一次调用,而任何库解决方案都至少有一个显式循环。
  • @JoshLee 我明白 - 但是,证据不支持速度是由于它是一个 ArrayList 的说法。 LinkedList::toArray(继承自 AbstractList::toArray)正在遍历列表以填充数组。它不是数组副本。然而,它远没有Stream 版本那么糟糕。
  • List 创建的流,没有filter 等。确实知道它的大小,并在toArray 实现中使用它。尽管如此,在未优化的执行中,它可能比任何硬编码操作都慢,仅仅是因为这个有缺陷的基准记录了加载、验证和初始化 Stream 类及其所有内部使用的类以及 @987654337 所需的时间@ 及其依赖项。
【解决方案2】:

在底层,流比普通数组复杂得多。编译器会变得更好,但目前,顺序 for 循环应该比流操作更快。

This article 有一些关于流管道的背景知识,这些管道用于实现流。它有助于理解其背后的复杂性。

流的优点是代码可以更清晰,更容易并行化。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-15
  • 1970-01-01
  • 2016-08-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多