Java 8とInfinite Streams
1概要
この記事では、
java.util.Stream
APIを見ていきます。無限のデータ/要素のストリームを操作するために、この構文をどのように使用できるかを見ていきましょう。
要素の無限のシーケンスに取り組む可能性は、ストリームが遅延するように構築されているという事実に基づいています。
この遅延は、ストリームに対して実行できる2つのタイプの操作(中間操作と最終操作)の分離によって実現されます。
2中間およびターミナル業務
-
すべての
Stream
操作は
intermediate
と
terminal
操作に分割され** 、ストリームパイプラインを形成するために結合されます。
ストリームパイプラインはソース(
Collection
、配列、ジェネレータ関数、I/Oチャネル、無限シーケンスジェネレータなど)で構成されます。その後にゼロ個以上の中間操作と端末操作が続きます。
2.1. 中間操作
中間操作は実行されず、いくつかの最終操作が呼び出されます。
それらは
Stream
実行のパイプラインを形成するように構成されています。メソッドによって
Stream
パイプラインに
intermediate
操作を追加できます。
-
filter()
-
map()
-
flatMap()
-
distinct()
-
sorted()
-
peek()
-
limit()
-
skip()
すべての中間操作は遅延しているため、処理結果が実際に必要になるまで実行されません。
基本的に、__中間操作は新しいストリームを返します。中間操作を実行しても実際には操作は実行されませんが、トラバース時に、指定された述部に一致する初期ストリームの要素を含む新しいストリームが作成されます。
そのため、
Stream
のトラバースは、パイプラインの
terminal
操作が実行されるまで開始されません。
これは非常に重要なプロパティです。無限ストリームにとって特に重要です。これは、
Terminal
操作が呼び出されたときにのみ実際に呼び出されるストリームを作成できるためです。
2.2.
端末
操作
端末操作は、結果または副作用を生成するためにストリームをトラバースします。
端末操作が実行されると、ストリームパイプラインは消費されたと見なされ、使用できなくなります。ほとんどすべての場合、端末操作は熱心で、データソースのトラバースとパイプラインの処理が完了する前に完了します。
端末操作の熱心さは無限ストリームに関して重要です。なぜなら、処理の時点で
Stream
が** によって適切に制限されているかどうかを慎重に考える必要があるからです。
端末
操作は以下のとおりです。
-
forEach()
-
forEachOrdered()
-
toArray()
-
reduce()
-
collect()
-
min()
-
max()
-
カウント()
-
anyMatch()
-
allMatch()
-
noneMatch()
-
findFirst()
-
findAny()
これらの各操作は、すべての中間操作の実行をトリガーします。
3無限ストリーム
中間
操作と
端末操作
という2つの概念を理解したので、Streamsの遅延を利用した無限ストリームを作成できます。
2つずつ増分される、ゼロから無限の要素のストリームを作成したいとしましょう。それから、端末操作を呼び出す前にそのシーケンスを制限する必要があります。
-
端末操作である
collect()
メソッドを実行する前に
limit()
メソッドを使用することが重要です。そうしないと、プログラムは無期限に実行されます。
----//given
Stream<Integer> infiniteStream = Stream.iterate(0, i -> i + 2);
//when
List<Integer> collect = infiniteStream
.limit(10)
.collect(Collectors.toList());
//then
assertEquals(collect, Arrays.asList(0, 2, 4, 6, 8, 10, 12, 14, 16, 18));
----
iterate()
メソッドを使用して無限ストリームを作成しました。それから
limit()
変換と
collect()
端末操作を呼び出しました。
それから、結果の
Listには、
Stream.__の遅延のため、無限シーケンスの最初の10個の要素があります。
4カスタムタイプの要素の無限ストリーム
無限のランダムな
UUID
のストリームを作成したいとしましょう。
Stream
APIを使用してこれを達成するための最初のステップは、これらの乱数の
Supplier
を作成することです。
Supplier<UUID> randomUUIDSupplier = UUID::randomUUID;
サプライヤを定義すると、
generate()
メソッドを使用して無限ストリームを作成できます。
Stream<UUID> infiniteStreamOfRandomUUID = Stream.generate(randomUUIDSupplier);
それから、そのストリームからいくつかの要素を取り出すことができます。プログラムを有限時間で終了させる場合は、
limit()
メソッドを使用することを忘れないでください。
List<UUID> randomInts = infiniteStreamOfRandomUUID
.skip(10)
.limit(10)
.collect(Collectors.toList());
最初の10個の結果を破棄して次の10個の要素を取得するには、
skip()
変換を使用します。
Supplier
インターフェースの関数を
Stream
の
generate()
メソッドに渡すことで、任意のカスタム型要素の無限ストリームを作成できます。
6.
Do-While
– ストリームウェイ
コードに単純なdo..whileループがあるとしましょう。
int i = 0;
while (i < 10) {
System.out.println(i);
i++;
}
カウンターを10回印刷しています。そのような構造は
Stream
APIを使って簡単に書くことができると期待できますし、理想的には
doWhile()
メソッドがストリームにあるでしょう。
残念ながら、そのようなメソッドはストリーム上にはありません。そして、標準的な
do-while
ループのような機能を実現したい場合は、
limit()
メソッドを使用する必要があります。
Stream<Integer> integers = Stream
.iterate(0, i -> i + 1);
integers
.limit(10)
.forEach(System.out::println);
より少ないコードで命令型whileループと同じ機能を実現しましたが、
limit()
関数の呼び出しは、
Stream
オブジェクトに対して
doWhile()
メソッドを使用した場合のようにはわかりません。
5結論
この記事では、
Stream API
を使用して無限ストリームを作成する方法について説明します。これらを
limit()–
などの変換と一緒に使用すると、一部のシナリオを理解しやすく実装しやすくなります。
これらすべての例をサポートするコードはhttps://github.com/eugenp/tutorials/tree/master/java-streams[GitHubプロジェクト]にあります。そのまま実行します。