复制流以避免“流已经被操作或关闭”

时间:2014-05-25 22:25:00

标签: java lambda java-8 java-stream

我想复制Java 8流,以便我可以处理两次。我可以collect作为列表并从中获取新流;

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

但我认为应该有一种更有效/更优雅的方式。

有没有办法复制流而不将其转换为集合?

我实际上正在使用Either s流,所以想要在移动到正确投影之前以一种方式处理左投影并以另一种方式处理。有点像这样(到目前为止,我被迫使用toList技巧)。

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

10 个答案:

答案 0 :(得分:78)

我认为你对效率的假设是倒退的。如果您只打算使用一次数据,那么您将获得巨大的效率回报,因为您不必存储它,并且流为您提供强大的“循环融合”优化,使您可以有效地通过管道传输整个数据。

如果要重新使用相同的数据,那么根据定义,您必须生成两次(确定性地)或存储它。如果它恰好在一个集合中,那就太好了;然后迭代两次便宜。

我们在设计中尝试了“分叉流”。我们发现支持这个有实际成本;它以不常见的情况为代价来承担普通案件(使用一次)。最大的问题是处理“当两个管道不以相同的速率消耗数据时会发生什么。”现在你还是要恢复缓冲了。这个功能显然没有发挥其重要作用。

如果您想重复操作相同的数据,请将其存储起来,或将操作结构化为消费者并执行以下操作:

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

您也可以查看RxJava库,因为它的处理模型更适合这种“流式分叉”。

答案 1 :(得分:52)

使用java.util.function.Supplier

来自http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/

  

重复播放

     

Java 8流无法重复使用。只要您调用任何终端操作,流就会关闭:

Stream<String> stream =

Stream.of("d2", "a2", "b1", "b3", "c")

.filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);    // ok

stream.noneMatch(s -> true);   // exception
     

在同一个流上的anyMatch之后调用noneMatch会导致以下异常:

java.lang.IllegalStateException: stream has already been operated upon or closed

at 

java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)

at 

java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)

at com.winterbe.java8.Streams5.test7(Streams5.java:38)

at com.winterbe.java8.Streams5.main(Streams5.java:28)
     

为了克服这个限制,我们必须为我们想要执行的每个终端操作创建一个新的流链,例如:我们可以创建一个流供应商来构建一个新流,其中已经设置了所有中间操作:

Supplier<Stream<String>> streamSupplier =

    () -> Stream.of("d2", "a2", "b1", "b3", "c")

            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok

streamSupplier.get().noneMatch(s -> true);  // ok
     

每次调用get()都会构建一个新流,我们将保存该流以调用所需的终端操作。

答案 2 :(得分:8)

我们为jOOλ中的流实施了duplicate()方法,这是我们为改进jOOQ的集成测试而创建的开源库。基本上,你可以写:

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

在内部,有一个缓冲区存储从一个流中消耗但从另一个流中消耗的所有值。如果您的两个流以相同的速率消耗,并且如果您可以忍受缺乏线程安全性,则可能效率高。

以下是该算法的工作原理:

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

More source code here

Tuple2可能与您的Pair类型相似,而SeqStream则有一些增强功能。

答案 3 :(得分:7)

您可以创建一个runnables流(例如):

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

failuresuccess是要应用的操作。然而,这将创建相当多的临时对象,并且可能不比从集合开始流式传输/迭代两次更有效。

答案 4 :(得分:5)

使用供应商为每个终止操作生成流。

Supplier <Stream<Integer>> streamSupplier=()->list.stream();

每当您需要该集合的流时, 使用streamSupplier.get()获取新流。

<强>示例:

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

答案 5 :(得分:3)

多次处理元素的另一种方法是使用Stream.peek(Consumer)

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer)可以根据需要链接多次。

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

答案 6 :(得分:2)

cyclops-react,我所贡献的库,有一个静态方法,允许你复制一个Stream(并返回一个jOOλ元组的流)。

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

请参阅注释,在现有Stream上使用重复时会产生性能损失。一个更高效的替代方案是使用Streamable: -

还有一个(懒惰)Streamable类,可以从Stream,Iterable或Array构造并多次重放。

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream(stream) - 可以用来创建一个Streamable,它将以一种可以跨线程共享的方式懒洋洋地填充它的后备集合。 Streamable.fromStream(stream)不会产生任何同步开销。

答案 7 :(得分:0)

对于此特定问题,您还可以使用分区。像

这样的东西
     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

答案 8 :(得分:0)

我们可以在读取或迭代流时使用Stream Builder。 这是流生成器的文档。

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

用例

假设我们有员工流,我们需要使用此流将员工数据写入excel文件,然后更新员工集合/表 [这只是显示Stream Builder使用的用例]:

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

答案 9 :(得分:0)

我遇到了类似的问题,可以想到从中创建流副本的三个不同的中间结构:List,数组和Stream.Builder。我写了一个基准测试程序,该程序建议从性能的角度来看,List比其他两个非常相似的程序慢30%。

转换为数组的唯一缺点是,如果您的元素类型是泛型类型(在我的情况下是),这将非常棘手。因此,我更喜欢使用Stream.Builder

我最终写了一个创建Collector的小函数:

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())的副本,这与流的惯用用法完全一致。