【问题标题】:What's the better way to add elements from a Stream to an existing List?将元素从 Stream 添加到现有列表的更好方法是什么?
【发布时间】:2017-01-22 13:11:18
【问题描述】:

我必须编写一些代码,将 Java 8 Stream 的内容多次添加到 List 中,但我无法确定最好的方法是什么。根据我在 SO(主要是这个问题:How to add elements of a Java8 stream into an existing List)和其他地方阅读的内容,我将其范围缩小到以下选项:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Accumulator<S, T> {


    private final Function<S, T> transformation;
    private final List<T> internalList = new ArrayList<T>();

    public Accumulator(Function<S, T> transformation) {
        this.transformation = transformation;
    }

    public void option1(List<S> newBatch) {
        internalList.addAll(newBatch.stream().map(transformation).collect(Collectors.toList()));
    }

    public void option2(List<S> newBatch) {
        newBatch.stream().map(transformation).forEach(internalList::add);
    }
}

这个想法是,对于Accumulator 的同一个实例,这些方法将被多次调用。选择是使用中间列表还是在流外部调用Collection.addAll() 或从流中为每个元素调用collection.add()

我倾向于选择更符合函数式编程精神的选项 2,并避免创建中间列表,但是,当 n 很大时调用 addAll() 而不是调用 add() 可能会有好处.

这两个选项中的一个明显优于另一个吗?

编辑:JB Nizet 有一个非常酷的answer,它会延迟转换,直到添加了所有批次。在我的情况下,需要立即执行转换。

PS:在我的示例代码中,我使用 transformation 作为需要在流上执行的任何操作的占位符

【问题讨论】:

  • 反汇编的字节码(javap)可能会帮助你弄清楚
  • 不要进行过早的优化。做任何更清洁的事情,并且只有在遇到性能问题时才使用分析器检查此代码。
  • 我认为拨打addAll() 没有任何好处。
  • 请注意,如果您进行并行流式处理,结果会有所不同,除非您将forEach() 更改为forEachOrdered()
  • 为什么要求“立即进行转换”?如果您关心“函数式编程的精神”,那么何时执行转换都无关紧要。

标签: java collections java-8 java-stream


【解决方案1】:

首先,您的第二个变体应该是:

public void option2(List<S> newBatch) {
    newBatch.stream().map(transformation).forEachOrdered(internalList::add);
}

是正确的。

除此之外,addAll 的优势

public void option1(List<S> newBatch) {
    internalList.addAll(newBatch.stream()
        .map(transformation).collect(Collectors.toList()));
}

没有实际意义,因为 Collector API 不允许 Stream 向 Collector 提供有关预期大小的提示,并且要求 Stream 评估每个元素的累加器函数,这在当前只是 ArrayList::add实施。

因此,在这种方法从 addAll 获得任何好处之前,它通过在 ArrayList 上反复调用 add 来填充 ArrayList,包括潜在的容量增加操作。所以你可以留在option2 而不后悔。

另一种方法是为临时集合使用流构建器:

public class Accumulator<S, T> {
    private final Function<S, T> transformation;
    private final Stream.Builder<T> internal = Stream.builder();

    public Accumulator(Function<S, T> transformation) {
        this.transformation = transformation;
    }

    public void addBatch(List<S> newBatch) {
        newBatch.stream().map(transformation).forEachOrdered(internal);
    }

    public List<T> finish() {
        return internal.build().collect(Collectors.toList());
    }
}

流构建器使用一个旋转缓冲区,在增加其容量时不需要复制内容,但解决方案仍然存在这样一个事实,即最终收集步骤涉及填充 ArrayList 而没有适当的初始容量(在当前实施)。

使用当前的实现,将完成步骤实现为更有效

public List<T> finish() {
    return Arrays.asList(internal.build().toArray(…));
}

但这需要调用者提供的IntFunction&lt;T[]&gt;(因为我们不能对泛型数组类型这样做),或者执行未经检查的操作(将Object[]假装为T[],这在这里可以,但仍然是一个令人讨厌的未经检查的操作)。

从 JDK 16 开始,您可以使用

public List<T> finish() {
    return internal.build().toList();
}

它返回一个不可变列表,没有泛型类型的困难,同时具有与Arrays.asList(internal.build().toArray(…)) 方法相似的性能特征(在典型实现中)。

【讨论】:

    【解决方案2】:

    最好的解决方案是第三个,完全避免内部列表。只需让流为您创建最终列表:

    假设您有一个 List&lt;List&lt;S&gt;&gt;,包含您的 N 个批次,必须对其应用相同的转换,您会这样做

    List<T> result = 
        batches.stream()
               .flatMap(batch -> batch.stream())
               .map(transformation)
               .collect(Collectors.toList());
    

    【讨论】:

    • 这是最好的选择..因为这将创建固定大小的列表。
    • 很好的答案,但它要求您可以推迟处理较早的批次,直到所有批次都准备好。情况可能并非总是如此。
    • 很好的答案,我不会想到这一点,尽管在我的情况下,我试图不推迟处理,正如@Andreas 提到的那样。我正在相应地编辑问题
    • @rana_stack 你认为Collectors.toList 创建了一个预定义的列表?离做到这一点还很遥远。看看code 或 Holger 的回答,它描述了实际发生的事情。
    猜你喜欢
    • 2021-01-24
    • 2019-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-09
    • 2021-09-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多