JavaストリームとVavrストリーム
1. 前書き
この記事では、Streamの実装がJavaとVavrでどのように異なるかを見ていきます。
この記事は、Java Stream APIとthe Vavr libraryの両方の基本に精通していることを前提としています。
2. 比較
どちらの実装も、レイジーシーケンスの同じ概念を表していますが、詳細が異なります。
Java Streams were built with robust parallelism in mind、並列化の簡単なサポートを提供します。 一方、Vavr実装は、データシーケンスを使用した便利な作業を優先し、並列処理のネイティブサポートを提供しません(ただし、インスタンスをJava実装に変換することで実現できます)。
これが、JavaストリームがSpliteratorのインスタンスによって支えられている理由です。はるかに古いIteratorへのアップグレードであり、Vavrの実装は前述のIteratorによって支えられています(少なくとも最新の実装の1つでは)。
どちらの実装もそのバッキングデータ構造に緩く結びついており、基本的にストリームが通過するデータのソースの上にあるファサードですが、Vavrの実装はIterator-based _、_であるため、ソースコレクションの同時変更を許容しません。
Javaによるストリームソースの処理により、ターミナルストリーム操作が実行される前にwell-behaved stream sourcesinを変更できます。
基本的な設計の違いにもかかわらず、Vavrはストリーム(およびその他のデータ構造)をJava実装に変換する非常に堅牢なAPIを提供します。
3. 追加機能
ストリームとその要素を処理するアプローチは、JavaとVavrの両方でストリームを操作する方法に興味深い違いをもたらします。
3.1. ランダム要素アクセス
便利なAPIと要素へのアクセスメソッドを提供することは、VavrがJava APIを真に照らしている領域の1つです。 たとえば、Vavrにはランダム要素アクセスを提供するメソッドがいくつかあります。
-
get() は、ストリームの要素へのインデックスベースのアクセスを提供します。
-
indexOf()は、標準のJavaList.と同じインデックスロケーション機能を提供します
-
insert()は、指定された位置でストリームに要素を追加する機能を提供します。
-
intersperse()は、指定された引数をストリームのすべての要素の間に挿入します。
-
find()は、ストリーム内からアイテムを見つけて返します。 Javaは、要素の存在をチェックするだけのnoneMatchedを提供します。
-
update() は、指定されたインデックスの要素を置き換えます。 これは、置換を計算する関数も受け入れます。
-
search()は、並べ替えられた stream内のアイテムを検索します(並べ替えられていないストリームは未定義の結果を生成します)
この機能は、検索に対して線形のパフォーマンスを持つデータ構造によって支えられていることを覚えておくことが重要です。
3.2. 並列処理と同時変更
VavrのストリームはJavaのparallel()メソッドのような並列処理をネイティブにサポートしていませんが、ソースVavrストリームの並列化されたJavaベースのコピーを提供するtoJavaParallelStream メソッドがあります。
Vavrストリームの比較的弱い領域は、Non-Interference.の原則に基づいています
簡単に言えば、 Javaストリームを使用すると、ターミナル操作が呼び出されるまで、基になるデータソースを変更できます。 特定のJavaストリームで端末操作が呼び出されていない限り、ストリームは基になるデータソースへの変更を取得できます。
List intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
Stream intStream = intList.stream(); //form the stream
intList.add(5); //modify underlying list
intStream.forEach(i -> System.out.println("In a Java stream: " + i));
最後の追加がストリームからの出力に反映されていることがわかります。 この動作は、変更がストリームパイプラインの内部であるか外部であるかにかかわらず一貫しています。
in a Java stream: 1
in a Java stream: 2
in a Java stream: 3
in a Java stream: 5
Vavrストリームはこれを許容しないことがわかりました。
Stream vavrStream = Stream.ofAll(intList);
intList.add(5)
vavrStream.forEach(i -> System.out.println("in a Vavr Stream: " + i));
私たちが得るもの:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at io.vavr.collection.StreamModule$StreamFactory.create(Stream.java:2078)
Javaの標準では、Vavrストリームは「正常」ではありません。 Vavrは、プリミティブなバッキングデータ構造でより適切に動作します。
int[] aStream = new int[]{1, 2, 4};
Stream wrapped = Stream.ofAll(aStream);
aStream[2] = 5;
wrapped.forEach(i -> System.out.println("Vavr looped " + i));
私たちに与える:
Vavr looped 1
Vavr looped 2
Vavr looped 5
3.3. 短絡操作とflatMap()
map操作のようなflatMap,は、ストリーム処理の中間操作です。どちらの実装もthe contract of intermediate stream operationsに従います。基になるデータ構造からの処理は、ターミナル操作が呼び出されるまで発生しません。
ただし、JDK 8および9はa bugを備えているため、flatMapの実装はこの契約を破り、findFirst またはlimitのような短絡中間操作と組み合わせると熱心に評価されます。
簡単な例:
Stream.of(42)
.flatMap(i -> Stream.generate(() -> {
System.out.println("nested call");
return 42;
}))
.findAny();
上記のスニペットでは、ネストされたStream.から単一の要素を取得するのではなく、flatMapが熱心に評価されるため、findAnyから結果が得られることはありません。
このバグの修正はJava10で提供されました。
VavrのflatMap には同じ問題はなく、機能的に類似した操作がO(1)で完了します。
Stream.of(42)
.flatMap(i -> Stream.continually(() -> {
System.out.println("nested call");
return 42;
}))
.get(0);
3.4. コアVavr機能
一部の領域では、JavaとVavrを1対1で比較することはできません。 Vavrは、Javaでは直接比類のない機能でストリーミングエクスペリエンスを強化します(または少なくともかなりの量の手作業が必要です)。
-
zip()は、ストリーム内のアイテムを、提供されたIterable. のアイテムとペアにします。この操作は、以前はJDK-8でサポートされていましたが、since been removed after build-93があります。
-
partition() は、述語を指定すると、ストリームのコンテンツを2つのストリームに分割します。
-
名前付きのpermutation() asは、ストリームの要素の順列(すべての可能な一意の順序)を計算します。
-
combinations() は組み合わせを示します(つまり、 ストリームの項目の可能な選択)。
-
groupByは、指定された分類子によって分類された、元のストリームの要素を含むMap ofストリームを返します。
-
Vavrのdistinctメソッドは、compareToラムダ式を受け入れるバリアントを提供することにより、Javaバージョンを改善します。
高度な機能のサポートはJavaSEストリームでは多少影響を受けませんが、Expression Language 3.0は、標準のJDKストリームよりもはるかに多くの機能のサポートを提供します。
4. ストリーム操作
Vavrでは、ストリームのコンテンツを直接操作できます。
-
既存のVavrストリームに挿入します
Stream vavredStream = Stream.of("foo", "bar", "baz");
vavredStream.forEach(item -> System.out.println("List items: " + item));
Stream vavredStream2 = vavredStream.insert(2, "buzz");
vavredStream2.forEach(item -> System.out.println("List items: " + item));
-
ストリームからアイテムを削除します
Stream removed = inserted.remove("buzz");
-
キューベースの操作
Vavrのストリームがキューによってバックアップされることにより、一定時間のprependおよびappend 操作を提供します。
ただし、changes made to the Vavr stream don’t propagate back to the data source that the stream was created from.
5. 結論
VavrとJavaにはどちらにも長所があり、Javaから安価な並列処理、Vavrから便利なストリーム操作という設計目標に対する各ライブラリの取り組みを示しました。
独自のストリームとJavaの間で相互に変換するためのVavrのサポートにより、多くのオーバーヘッドなしで同じプロジェクト内の両方のライブラリの利点を引き出すことができます。
このチュートリアルのソースコードはover on Githubで入手できます。