制作流副本的最有效方法是什么?

时间:2019-01-22 14:36:10

标签: java java-8 java-stream

我有一种对流执行处理的方法。该处理的一部分需要在一个锁的控制下完成-一个用于处理所有元素的锁定部分-但其中一些不是(并且不应该这样做,因为这可能会很耗时)。所以我不能只说:

Stream<V> preprocessed = Stream.of(objects).map(this::preProcess);
Stream<V> toPostProcess;
synchronized (lockObj) {
    toPostProcess = preprocessed.map(this::doLockedProcessing);
}
toPostProcess.map(this::postProcess).forEach(System.out::println);

因为对doLockedProcessing的调用仅在调用终端操作forEach且在锁之外时才执行。

所以我想我需要在每个阶段使用终端操作复制流的副本,以便在正确的时间完成正确的位。像这样:

Stream<V> preprocessed = Stream.of(objects).map(this::preProcess).copy();
Stream<V> toPostProcess;
synchronized (lockObj) {
    toPostProcess = preprocessed.map(this::doLockedProcessing).copy();
}
toPostProcess.map(this::postProcess).forEach(System.out::println);

当然,copy()方法不存在,但是如果这样做,它将对流执行终端操作并返回包含所有相同元素的新流。

我知道实现此目的的几种方法:

(1)通过数组(如果元素类型是泛型类型,则并不容易):

copy = Stream.of(stream.toArray(String[]::new));

(2)通过列表:

copy = stream.collect(Collectors.toList()).stream();

(3)通过流构建器:

Stream.Builder<V> builder = Stream.builder();
stream.forEach(builder);
copy = builder.build();

我想知道的是:在时间和内存方面,哪种方法最有效?还是有另一种更好的方法?

3 个答案:

答案 0 :(得分:3)

我认为您已经提到了所有可能的选择。没有其他结构方式可以满足您的需求。首先,您必须使用原始流。然后,创建一个新的流,获取锁定并使用此新流(因此调用您的锁定操作)。最后,创建一个更新的流,释放锁,然后继续处理此更新的流。

在您考虑的所有选项中,我将使用第三个选项,因为它可以处理的元素数量仅受内存限制,这意味着它没有隐式的最大大小限制,例如{{1} }(具有大约ArrayList个元素)。

不用说,就时间和空间而言,这将是一个非常昂贵的操作。您可以按照以下步骤进行操作:

Integer.MAX_VALUE

请注意,我只使用了一个Stream<V> temp = Stream.of(objects) .map(this::preProcess) .collect(Stream::<V>builder, Stream.Builder::accept, (b1, b2) -> b2.build().forEach(b1)) .build(); synchronized (lockObj) { temp = temp .map(this::doLockedProcessing) .collect(Stream::<V>builder, Stream.Builder::accept, (b1, b2) -> b2.build().forEach(b1)) .build(); } temp.map(this::postProcess).forEach(System.out::println); 实例Stream,以便可以根据需要对中间流(及其生成器)进行垃圾收集。


正如@Eugene在评论中所建议的那样,最好使用一种实用程序方法来避免代码重复。这是这样的方法:

temp

然后,您可以按照以下方法进行操作:

public static <T> Stream<T> copy(Stream<T> source) {
    return source.collect(Stream::<T>builder,
                          Stream.Builder::accept,
                          (b1, b2) -> b2.build().forEach(b1))
                 .build();
}

答案 1 :(得分:2)

我创建了一个基准测试,比较了这三种方法。这表明使用List作为中间存储区比使用类似的数组或Stream.Builder慢约30%。因此,我之所以使用Stream.Builder是因为在元素类型是泛型类型的情况下,转换为数组很棘手。

我最终编写了一个小函数,该函数创建了一个Collector,它使用Stream.Builder作为中间存储:

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

然后我可以通过执行str来复制任何流str.collect(copyCollector())的副本,这与流的惯用用法完全一致。

我发布的原始代码如下:

Stream<V> preprocessed = Stream.of(objects).map(this::preProcess).collect(copyCollector());
Stream<V> toPostProcess;
synchronized (lockObj) {
    toPostProcess = preprocessed.map(this::doLockedProcessing).collect(copyCollector());
}
toPostProcess.map(this::postProcess).forEach(System.out::println);

答案 2 :(得分:0)

doLockedProcessing同步地包装。这是一种方法:

class SynchronizedFunction<T, R> {
    private Function<T, R> function;
    public SynchronizedFunction(Function<T, R> function) {
        this.function = function;
    }
    public synchronized R apply(T t) {
        return function.apply(t);
    }
}

然后在您的信息流中使用它:

stream.parellel()
  .map(this:preProcess)
  .map(new SynchronizedFunction<>(this::doLockedProcessing))
  .forEach(this::postProcessing)

这将顺序处理锁定的代码,否则将并行处理。