ストリームをコピーして、「ストリームはすでに操作されているか、閉じられています」


121

2回処理できるように、Java 8ストリームを複製したいと思います。私ができるcollectことから、新たなストリームを取得し、リストのように。

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

しかし、もっと効率的でエレガントな方法があるべきだと思います。

コレクションに変換せずにストリームをコピーする方法はありますか?

私は実際にはEithersのストリームで作業しているので、右のプロジェクションに移動して別の方法で処理する前に、左のプロジェクションを1つの方法で処理したいとします。このようなもの(これまでのところ、私は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 2017

場合によっては、無限ストリームを処理している場合、それをコレクションに変換することは選択肢になりません。ここでメモ化の代替案を見つけることができます:dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

回答:


88

効率性についてのあなたの仮定は、ちょっと後ろ向きだと思います。データを1度だけ使用する場合は、データを保存する必要がないため、この大きな効率の見返りが得られます。また、ストリームにより、強力な「ループフュージョン」最適化により、データ全体をパイプラインに効率的に流すことができます。

同じデータを再利用する場合は、定義上、データを2回(確定的に)生成するか、保存する必要があります。コレクションに既に含まれている場合は、すばらしいです。その後、2回繰り返すのは安価です。

「フォークストリーム」を使用してデザインを実験しました。私たちが見つけたのは、これをサポートすることには実際のコストがかかるということです。まれなケースを犠牲にして、よくあるケースに負担をかけました(1回使用)。大きな問題は、「2つのパイプラインが同じ速度でデータを消費しないとどうなるか」を処理することでした。これで、とにかくバッファリングに戻りました。これは明らかにその重さを担っていない機能でした。

同じデータを繰り返し操作する場合は、データを保存するか、コンシューマーとして操作を構造化して、次の操作を行います。

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

RxJavaライブラリーを調べることもできます。RxJavaライブラリーの処理モデルは、この種の「ストリーム分岐」に適しています。


1
多分私は「効率」を使うべきではなかったのですが、データ(toList)をすぐに保存して処理できるのであれば、なぜストリームに煩わされる(何も保存しない)理由がわかります(Eitherケース例です)?
Toby

11
ストリームは表現力豊か効率的です。これらは、コードを読み取る方法で多くの偶発的な詳細(たとえば、中間結果)なしで複雑な集約操作をセットアップできるという点で表現力があります。また、(一般的に)データに対して1つのパスを作成し、中間結果コンテナにデータを入力しないため、効率的です。これら2つのプロパティにより、多くの状況で魅力的なプログラミングモデルになります。もちろん、すべてのプログラミングモデルがすべての問題に当てはまるわけではありません。それでも、ジョブに適切なツールを使用しているかどうかを判断する必要があります。
Brian Goetz 2015年

1
しかし、ストリームを再利用できないと、2つの異なる方法でストリームを処理するために、開発者が中間結果(収集)を保存する必要があります。ストリームが2回以上生成されるという意味(それを収集しない限り)は明らかなようです。それ以外の場合、collectメソッドは必要ありません。
Niall Connaughton 2017年

@NiallConnaughton私はあなたのポイントが欲しいのかわかりません。2回トラバースしたい場合は、誰かが保存するか、再生成する必要があります。誰かがそれを2度必要とする場合に備えて、ライブラリがそれをバッファするように提案していますか?それはばかげたことでしょう。
Brian Goetz 2017年

ライブラリがそれをバッファリングする必要があることを示唆しているわけではありませんが、ストリームを1回限りにすることで、シードストリームを再利用したい(つまり、定義に使用した宣言ロジックを共有したい)人々に、複数の派生ストリームを構築していずれかを収集するように強制しますシードストリーム、またはシードストリームの複製を作成するプロバイダーファクトリにアクセスできます。どちらのオプションにも問題点があります。この回答には、トピックに関する詳細が含まれています:stackoverflow.com/a/28513908/114200
Niall Connaughton 2017年

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っぽい。
dylaniato

が「高額」の方法で構築されているSupplier場合の使用に関する注意事項Streamは、への呼び出しごとにそのコストを支払うことSupplier.get()です。つまり、データベースクエリの場合...そのクエリは毎回行われます
ジュリアン

IntStreamを使用しても、mapToの後にこのパターンをたどることはできないようです。私はそれSet<Integer>をusing に変換し直さなければならないことに気づきましたcollect(Collectors.toSet())...そしてそれに対していくつかの操作をしました。私が欲しかったmax()し、特定の値が2つの操作として設定されている場合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

jOOQの統合テストを改善するために作成したオープンソースライブラリduplicate()であるjOOλにストリームのメソッドを実装しました。基本的に、あなたはただ書くことができます:

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

内部的には、1つのストリームからは消費されたが、他のストリームからは消費されなかったすべての値を格納するバッファーがあります。これは、2つのストリームがほぼ同じ速度で消費され、スレッドセーフの欠如に耐えることができる場合と同じくらい効率的です。

アルゴリズムの仕組みは次のとおりです。

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
このソリューションはスレッドセーフではありません。ストリームの1つを別のスレッドに渡すことはできません。単一のスレッドで両方のストリームを同じレートで消費でき、実際には2つの異なるストリームが必要な場合、シナリオは実際にはありません。同じストリームから2つの結果を生成する場合は、(JOOLに既にある)結合コレクターを使用する方がはるかに良いでしょう。
Tagir Valeev

@TagirValeev:スレッドの安全性についてはあなたの言うとおりです。コレクターを組み合わせてこれをどのように行うことができますか?
Lukas Eder

1
つまり、誰かがこのように同じストリームを2回使用したい場合は、を使用する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/reducing1つを使用すると、他のストリーム操作をコレクターとして処理し、非常に異なる方法で要素を処理して、単一の結果タプルを作成できます。そのため、一般に、重複することなくストリームを消費して多くのことを一度に実行でき、並列処理に適しています。
Tagir Valeev、2015

2
この場合でも、ストリームを1つずつ減らしていきます。したがって、とにかくストリーム全体をフードの下のリストに収集する洗練されたイテレータを導入して、人生を困難にする意味はありません。リストに明示的に収集するだけで、OPから通知されるようにリストから2つのストリームを作成できます(コード行の数は同じです)。まあ、最初の削減が短絡である場合にのみ、いくつかの改善があるかもしれませんが、それはOPの場合ではありません。
Tagir Valeev、2015

1
@maaartinus:ありがとう、良いポインタ。ベンチマークの問題を作成しました。私はそれをoffer()/ poll()API に使用しましたがArrayDeque、同じことをするかもしれません。
Lukas Eder 2017年

7

たとえば、実行可能ファイルのストリームを作成できます。

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

適用する操作の場所failureと場所success。ただし、これによりかなりの数の一時オブジェクトが作成され、コレクションから開始して、それを2回ストリーミング/反復するよりも効率的でない場合があります。


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には、ストリームを複製できる(そしてストリームのjOOλタプルを返す)静的メソッドがあります。

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

コメントを参照してください。既存のストリームで複製を使用すると、パフォーマンスが低下します。より高性能な代替手段は、Streamableを使用することです:-

また、Stream、Iterable、またはArrayから構築して複数回再生できる(遅延)Streamableクラスもあります。

    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)は同期のオーバーヘッドを引き起こしません。


2
そしてもちろん、結果のストリームは、CPU /メモリのオーバーヘッドが大きく、並列パフォーマンスが非常に低いことに注意してください。また、このソリューションはスレッドセーフではありません(結果のストリームの1つを別のスレッドに渡して、安全に並列処理することはできません)。List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(OPが示唆するように)はるかにパフォーマンスが高く、安全です。また、あなたがcyclop-streamsの作者であることを回答で明示的に開示してください。これを読んでください。
Tagir Valeev

私が作者であることを反映して更新されました。また、それぞれのパフォーマンス特性について説明することもできます。上記の評価は、StreamUtils.duplicateにかなり当てはまります。StreamUtils.duplicateは、1つのStreamから別のStreamにデータをバッファリングすることで機能し、CPUとメモリの両方のオーバーヘッドが発生します(ユースケースによって異なります)。ただし、Streamable.of(1,2,3)の場合、毎回新しいStreamがArrayから直接作成され、並列パフォーマンスを含むパフォーマンス特性は、通常作成されるStreamと同じになります。
John McClean

また、StreamからStreamableインスタンスを作成できるAsStreamableクラスもありますが、作成時にStreamableをサポートするコレクションへのアクセスを同期します(AsStreamable.synchronizedFromStream)。スレッド間での使用により適したものにする(必要な場合-ストリームが同じスレッドで作成および再利用される時間の99%を想像します)。
John McClean

こんにちはタギル-あなたはあなたがあなたが競合する図書館の作者であることもあなたのコメントで開示すべきではありませんか?
John McClean

1
私のライブラリにはストリームを複製する機能がないので(私はそれが役に立たないと思うから)、コメントは回答ではなく、ここでは自分のライブラリを宣伝していません。もちろん、自分のライブラリに関連するソリューションを提案するときは、常に自分が作者であると明示的に言います。
Tagir Valeev

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<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

同様の問題があり、ストリームのコピーを作成するための3つの異なる中間構造、a List、配列、およびを考えることができましたStream.Builder。私は小さなベンチマークプログラムを作成しました。これは、パフォーマンスの観点から、Listかなり似ている他の2つよりも約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())と感じることにより、任意のストリームのコピーを作成できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.