Java 8ストリームを複製して、2回処理できるようにします。リストとしてcollect
して、そこから新しいストリームを取得できます。
// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff
しかし、もっと効率的でエレガントな方法があるべきだと思います。
ストリームをコレクションにしないでストリームをコピーする方法はありますか?
私は実際にEither
sのストリームで作業しているので、左投影を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 -> ... );
効率性についてのあなたの仮定は、ちょっと逆向きだと思います。データを一度だけ使用する場合、この巨大な効率の見返りが得られます。データを保存する必要がないため、ストリームは強力な「ループ融合」最適化を提供し、データ全体をパイプラインに効率的に流します。
同じデータを再利用する場合は、定義により、2回(確定的に)生成するか、保存する必要があります。既にコレクションに含まれている場合は、素晴らしいです。それを2回繰り返すのは安価です。
「分岐ストリーム」を使用して設計を実験しました。私たちが見つけたのは、これをサポートするためには実際のコストがかかるということです。珍しいケースを犠牲にして、よくあるケース(1回使用)に負担をかけました。大きな問題は、「2つのパイプラインが同じレートでデータを消費しないとどうなるか」を扱うことでした。とにかくバッファリングに戻ります。これは、明らかにその重みを持たない機能でした。
同じデータを繰り返し操作する場合は、そのデータを保存するか、操作をコンシューマとして構造化し、次の操作を実行します。
stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });
また、RxJavaライブラリを調べることもできます。その処理モデルは、この種の「ストリームフォーク」により適しているためです。
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()
を呼び出すたびに、目的の端末操作を呼び出すために保存する新しいストリームが作成されます。
jOOQ の統合テストを改善するために作成したオープンソースライブラリ jOOλ で、ストリームのduplicate()
メソッドを実装しました。基本的に、あなたは書くことができます:
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
型に似ていますが、Seq
はStream
にいくつかの拡張が加えられています。
実行可能ファイルのストリームを作成できます(たとえば)。
results.stream()
.flatMap(either -> Stream.<Runnable> of(
() -> failure(either.left()),
() -> success(either.right())))
.forEach(Runnable::run);
ここで、failure
およびsuccess
は適用する操作です。ただし、これによりかなりの数の一時オブジェクトが作成され、コレクションから開始して2回ストリーミング/反復するよりも効率的ではない場合があります。
サプライヤーを使用して、終了操作ごとにストリームを生成します。
Supplier <Stream<Integer>> streamSupplier=()->list.stream();
そのコレクションのストリームが必要なときはいつでも、streamSupplier.get()
を使用して新しいストリームを取得します。
例:
streamSupplier.get().anyMatch(predicate);
streamSupplier.get().allMatch(predicate2);
要素を複数回処理する別の方法は、 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()));
cyclops-react 、私が提供しているライブラリには、Streamを複製できる(そしてjOOλTuple of Streamsを返す)静的メソッドがあります。
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)は、同期のオーバーヘッドを引き起こしません。
同様の問題があり、ストリームのコピーを作成する3つの異なる中間構造、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.collect(copyCollector())
を実行することで、任意のストリームstr
のコピーを作成できます。これは、ストリームの慣用的な使用法に非常によく従っています。
この特定の問題には、パーティション分割も使用できます。何かのようなもの
// 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)
ストリームの読み取りまたは反復時に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();