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


121

我想复制一个Java 8流,以便可以处理两次。我可以collect列出并从中获得新的信息流;

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

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

有没有一种方法可以复制流而不将其转换为集合?

我实际上正在使用Eithers 流,因此想要先处理左侧投影,然后再移至右侧投影并以另一种方式处理。有点像这样(到目前为止,我被迫使用这种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 -> ... );

您能否详细介绍“一种处理方式” ...您是否在消耗对象?映射他们?partitionBy()和groupingBy()可以直接将您带到2个以上的列表,但是您可能会受益于首先映射或仅在forEach()中拥有决策叉。
AjahnCharles

在某些情况下,如果我们要处理无限流,则不能将其转换为Collection。您可以在此处找到一种替代的记忆方式:dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Answers:


88

我认为您对效率的假设有点倒退。如果您只需要使用一次数据,那么您将获得巨大的效率回报,因为您不必存储数据,而流为您提供了强大的“循环融合”优化,可以使整个数据有效地流经管道。

如果要重复使用相同的数据,那么根据定义,您要么必须生成两次(确定性地),要么将其存储。如果它已经碰巧在收藏中,那就太好了;然后迭代两次很便宜。

我们在“分叉流”中进行了设计实验。我们发现,对此进行支持需要付出实际成本;它负担了普通案例(一次使用)的负担,却以罕见案例为代价。最大的问题是处理“当两个管道不以相同的速率使用数据时会发生什么”。现在您无论如何都要返回缓冲。这个功能显然没有发挥作用。

如果要重复对相同的数据进行操作,请存储它或将其结构化为“使用者”,然后执行以下操作:

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

您可能还需要研究RxJava库,因为它的处理模型更适合于这种“流派生”。


1
也许我不应该用“效率”,我是那种在我为什么会用流(而不是存储任何)理会,如果我要做的就是立刻存储数据得到的(toList),以能够处理它(的Either情况下,作为例子)?
Toby

11
流既富有表现力高效。它们具有表现力,因为它们使您可以设置复杂的聚合操作,而不会在读取代码的过程中出现很多偶然的细节(例如,中间结果)。它们也是有效的,因为它们(通常)对数据进行一次传递,并且不填充中间结果容器。这两个属性一起使它们成为许多情况下有吸引力的编程模型。当然,并非所有的编程模型都能解决所有问题。您仍然需要确定您是否在使用适合该工作的工具。
Brian Goetz 2015年

1
但是无法重用流会导致以下情况:开发人员被迫存储中间结果(收集)以便以两种不同方式处理流。流不止一次生成的含义(除非您收集它)似乎很明显-因为否则您将不需要collect方法。
Niall Connaughton

@NiallConnaughton我不确定您要说的是什么。如果要遍历两次,则必须有人存储它,或者必须重新生成它。您是否建议图书馆应该缓冲它,以防万一有人需要两次?那太傻了。
Brian Goetz

并不是建议库应该对其进行缓冲,而是说通过一次性使用流,它会迫使想要重用种子流(即:共享用于定义它的声明性逻辑)的人构建多个派生流以收集种子流,或有权访问提供者工厂,该工厂将创建种子流的副本。两种选择都有其痛点。这个答案有关于该主题的更多详细信息:stackoverflow.com/a/28513908/114200
Niall Connaughton

73

您可以将局部变量与一起使用,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

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
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
不错而优雅的解决方案。比最受支持的解决方案多得多的java8-ish。
dylaniato

只是有关使用的说明(Supplier如果Stream是以“昂贵”的方式构建的),您需要为的每次调用支付该费用Supplier.get()。即如果数据库查询...每次都执行该查询
Julien

尽管使用了IntStream,但在mapTo之后似乎无法遵循这种模式。我发现我不得不将其转换回Set<Integer>using collect(Collectors.toSet())...,并对此进行了一些操作。我想要max()并且是否将一个特定值设置为两个操作...filter(d -> d == -1).count() == 1;
JGFMK

16

使用a Supplier来生成每个终止操作的流。

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

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

例子:

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

支持您,因为您是第一个在这里指出供应商的人。
EnzoBnl

9

我们已经duplicate()jOOλ中实现了一种流方法,jOOλ是我们创建的一个开放源代码库,用于改进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()));
}

更多源代码在这里

Tuple2大概是喜欢你的Pair类型,而SeqStream一些增强功能。


2
此解决方案不是线程安全的:您无法将流之一传递给另一个线程。当两个流可以在单个线程中以相等的速率消耗并且您实际上需要两个不同的流时,我真的看不到任何场景。如果要从同一流中产生两个结果,最好使用组合收集器(JOOL中已经有)。
塔吉尔·瓦列夫

@TagirValeev:关于线程安全,您说的很对。合并收集器怎么办?
卢卡斯·埃德

1
我的意思是,如果有人想像这样两次使用相同的流Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());,最好这样做Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));。使用Collectors.mapping/reducing一个可以以完全不同的方式将其他流操作表示为收集器和处理元素,从而创建单个结果元组。因此,通常您可以做很多事情而无需复制就消耗一次流,这将是并行友好的。
Tagir Valeev 2015年

2
在这种情况下,您仍将减少一个流。因此,引入复杂的迭代器毫无意义,因为无论如何它将迭代整个迭代器收集到引擎盖下的列表中。您可能只是明确地收集到列表,然后按照OP的指示从列表中创建两个流(这是相同数量的代码行)。好吧,如果第一个减少是短路,您可能只会有所改善,但不是OP情况。
Tagir Valeev 2015年

1
@maaartinus:谢谢,很好的指针。我为基准测试创建了一个问题。我将其用于offer()/ poll()API,但ArrayDeque可能会做同样的事情。
卢卡斯·埃德

7

您可以创建可运行流(例如):

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

在哪里failuresuccess对应用的操作。但是,这将创建许多临时对象,并且可能没有效率比从集合开始并对其进行流式处理/迭代两次的效率更高。


4

多次处理元素的另一种方法是使用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()));

这似乎是不应该偷看用于本(见softwareengineering.stackexchange.com/a/308979/195787
HectorJ

2
@HectorJ另一个线程是关于修改元素的。我认为这里没有做。
马丁

2

我贡献的一个库cyclops-react具有一个静态方法,该方法将允许您复制Stream(并返回jOOλStreams Tuple)。

    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将以可在线程之间共享的方式延迟填充其后备集合。Streamable.fromStream(stream)将不会产生任何同步开销。


2
并且,当然应该注意,生成的流具有显着的CPU /内存开销和非常差的并行性能。同样,此解决方案也不是线程安全的(您不能将结果流之一传递到另一个线程并并行安全地对其进行处理)。List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(如OP所建议的那样)它将具有更高的性能和安全性。还请在答案中明确披露您是cyclop-streams的作者。阅读
Tagir Valeev 2015年

更新以反映我是作者。讨论每种性能特征也是一个好方法。以上对StreamUtils.duplicate的评估非常重要。StreamUtils.duplicate的工作原理是将数据从一个Stream缓冲到另一个Stream,从而造成CPU和内存开销(取决于使用情况)。但是,对于Streamable.of(1,2,3),每次都直接从数组创建一个新的Stream,并且性能特征(包括并行性能)将与通常创建的Stream相同。
John McClean 2015年

此外,还有一个AsStreamable类,该类允许从Stream创建Streamable实例,但在创建Streamable时将其同步访问支持Backstream的集合(AsStreamable.synchronizedFromStream)。使它更适合跨线程使用(如果这是您所需要的-我可以想象99%的时间在同一线程上创建和重用流)。
约翰·麦克林

嗨,塔吉尔-您是否也不应在评论中透露您是竞争图书馆的作者?
John McClean 2015年

1
评论不是答案,我也不会在这里做广告,因为我的图书馆没有复制信息流的功能(只是因为我认为它没有用),所以我们不在这里竞争。当然,当我提出涉及我的图书馆的解决方案时,我总是明确地说我是作者。
Tagir Valeev 2015年

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)

0

在读取或迭代流时,我们可以使用Stream Builder。这是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();

0

我有一个类似的问题,可以想到从中创建流副本的三个不同的中间结构:a List,数组和a 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())来复制任何流,这与流的惯用用法完全一致。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.