KotlinのJava 8 Stream APIアナロジー

KotlinのJava 8ストリームAPIの類推

1. 前書き

Java 8では、コレクション階層にStreamsの概念が導入されました。 これらは、プロセスを機能させるためにいくつかの関数型プログラミングの概念を利用して、非常に読みやすい方法でデータの非常に強力な処理を可能にします。

Kotlinイディオムを使用して、同じ機能をどのように実現できるかを調査します。 また、プレーンJavaでは利用できない機能についても説明します。

2. Javaと対 コトリン

Java 8では、新しいファンシーAPIは、java.util.stream.Streamインスタンスと対話する場合にのみ使用できます。

良い点は、すべての標準コレクション(java.util.Collectionを実装するもの)には、Streamインスタンスを生成できる特定のメソッドstream()があることです。

StreamCollection.ではないことを覚えておくことが重要ですIt does not implement java.util.Collection and it does not implement any of the normal semantics of Collections in Java.Collectionから派生しているという点で、1回限りのIteratorに似ていますそして、それを処理するために使用され、表示される各要素に対して操作を実行します。

最初に変換する必要のないIn Kotlin, all collection types already support these operations。 変換が必要になるのは、コレクションのセマンティクスが間違っている場合のみです。たとえば、Setには一意の要素がありますが、順序付けられていません。

これの利点の1つは、CollectionからStream,への最初の変換や、Streamからコレクションへの最終変換の必要がないことです–collect()呼び出し。

たとえば、Java 8で、私たちは次のように記述する必要があります:

someList
  .stream()
  .map() // some operations
  .collect(Collectors.toList());

Kotlinの同等物は非常に単純です:

someList
  .map() // some operations

Additionally, Java 8 Streams are also non-reusable.Streamが消費されると、再度使用することはできません。

たとえば、次のようには機能しません。

Stream someIntegers = integers.stream();
someIntegers.forEach(...);
someIntegers.forEach(...); // an exception

Kotlinでは、これらがすべて通常のコレクションであるという事実は、この問題が発生しないことを意味します。 Intermediate state can be assigned to variables and shared quicklyであり、期待どおりに機能します。

3. 怠惰なシーケンス

Java 8Streamsの重要な点の1つは、それらが遅延評価されることです。 これは、必要以上の作業が実行されないことを意味します。

これは、Stream,内の要素に対して潜在的にコストのかかる操作を実行している場合、または無限のシーケンスでの作業を可能にする場合に特に役立ちます。

たとえば、IntStream.generateは、潜在的に無限のStreamの整数を生成します。 その上でfindFirst()を呼び出すと、最初の要素が取得され、無限ループに陥ることはありません。

In Kotlin, collections are eager, rather than lazy。 ここでの例外はSequenceで、これは遅延評価します。

次の例に示すように、これは注意すべき重要な違いです。

val result = listOf(1, 2, 3, 4, 5)
  .map { n -> n * n }
  .filter { n -> n < 10 }
  .first()

これのKotlinバージョンは、5つのmap()操作と5つのfilter()操作を実行してから、最初の値を抽出します。 Java 8バージョンは、最後の操作の観点からはこれ以上必要ないため、1つのmap()と1つのfilter()のみを実行します。

All collections in Kotlin can be converted to a lazy sequence using the asSequence() method

上記の例でListの代わりにSequenceを使用すると、Java8と同じ数の操作が実行されます。

4. Java 8Stream操作

Java 8では、Streamの操作は2つのカテゴリに分類されます。

  • 中間および

  • ターミナル

中間演算は、基本的に1つのStreamを別のStreamに遅延変換します。たとえば、すべての整数のStreamをすべて偶数の整数のStreamに変換します。

ターミナルオプションは、Streamメソッドチェーンの最後のステップであり、実際の処理をトリガーします。

Kotlinにはそのような区別はありません。 代わりに、these are all just functions that take the collection as input and produce a new output.

Kotlinで熱心なコレクションを使用している場合、これらの操作はすぐに評価されることに注意してください。これは、Javaと比較すると驚くべきことです。 If we need it to be lazy, remember to convert to a Sequence first.

4.1. 中間操作

Almost all intermediate operations from the Java 8 Streams API have equivalents in Kotlin。 ただし、これらは、Sequenceクラスの場合を除いて、中間操作ではありません。これは、入力コレクションの処理からコレクションが完全に入力されるためです。

これらの操作のうち、まったく同じように機能するものがいくつかあります–filter()map()flatMap()distinct()sorted() –そしていくつかは異なる名前でのみ同じ–limit()takeになり、skip()drop()になりました。 例えば:

val oddSquared = listOf(1, 2, 3, 4, 5)
  .filter { n -> n % 2 == 1 } // 1, 3, 5
  .map { n -> n * n } // 1, 9, 25
  .drop(1) // 9, 25
  .take(1) // 9

これにより、単一の値「9」「3²」が返されます。

Some of these operations also have an additional version – suffixed with the word “To” –新しいコレクションを生成する代わりに、提供されたコレクションに出力します。

これは、いくつかの入力コレクションを同じ出力コレクションに処理するのに役立ちます。たとえば、次のとおりです。

val target = mutableList()
listOf(1, 2, 3, 4, 5)
  .filterTo(target) { n -> n % 2 == 0 }

これにより、値「2」と「4」がリスト「target」に挿入されます。

The only operation that does not normally have a direct replacement is peek() – Java 8で使用され、フローを中断することなく、処理パイプラインの途中でStreamのエントリを反復処理します。

熱心なコレクションの代わりに怠惰なSequenceを使用している場合は、peek関数を直接置き換えるonEach()関数があります。 ただし、これはこの1つのクラスにのみ存在するため、どのタイプを使用して動作するかを認識する必要があります。

There are also some additional variations on the standard intermediate operations that make life easier。 たとえば、filter操作には、追加のバージョンfilterNotNull()filterIsInstance()filterNot()、およびfilterIndexed()があります。

例えば:

listOf(1, 2, 3, 4, 5)
  .map { n -> n * (n + 1) / 2 }
  .mapIndexed { (i, n) -> "Triangular number $i: $n" }

これにより、最初の5つの三角形の数が「三角形の数3:6」の形式で生成されます。

もう1つの重要な違いは、flatMap操作の動作方法にあります。 Java 8では、この操作はStreamインスタンスを返すために必要ですが、Kotlinでは任意のコレクションタイプを返すことができます。 これにより、作業が容易になります。

例えば:

val letters = listOf("This", "Is", "An", "Example")
  .flatMap { w -> w.toCharArray() } // Produces a List
  .filter { c -> Character.isUpperCase(c) }

Java 8では、これを機能させるには、2行目をArrays.toStream()でラップする必要があります。

4.2. ターミナル運営

collectを除いて、Java 8 StreamsAPIのすべての標準ターミナル操作はKotlinで直接置き換えられます。

それらのいくつかは異なる名前を持っています:

  • anyMatch()any()

  • allMatch()all()

  • noneMatch()none()

それらのいくつかには、Kotlinの違いを処理するための追加のバリエーションがあります。first()firstOrNull()があり、コレクションが空の場合はfirstがスローされますが、それ以外の場合はnull許容型は返されません。

興味深いケースはcollectです。 Java 8はこれを使用して、提供された戦略を使用して、すべてのStream要素をいくつかのコレクションに収集できるようにします。

これにより、任意のCollectorを指定できます。これは、コレクション内のすべての要素で提供され、ある種の出力を生成します。 これらはCollectorsヘルパークラスから使用されますが、必要に応じて独自に作成できます。

In Kotlin there are direct replacements for almost all of the standard collectors available directly as members on the collection object itself –コレクターが提供されている場合、追加の手順は必要ありません。

ここでの1つの例外は、summarizingDouble /summarizingInt /summarizingLongメソッドです。これらのメソッドは、平均、カウント、最小、最大、および合計をすべて一度に生成します。 これらはそれぞれ個別に生産できますが、明らかにコストが高くなります。

または、for-eachループを使用して管理し、必要に応じて手動で処理することもできます–it is unlikely we will need all 5 of these values at the same time, so we only need to implement the ones that are important.

5. Kotlinでの追加操作

Kotlinは、コレクションにいくつかの追加操作を追加します。これは、Java8では自分で実装しないと不可能です。

これらの一部は、前述のように、単に標準操作の拡張機能です。 たとえば、新しいコレクションを返すのではなく、結果が既存のコレクションに追加されるように、すべての操作を実行できます。

また、多くの場合、ラムダに問題の要素だけでなく、要素のインデックスも提供することができます-順序付けられたコレクションのために、インデックスは意味があります。

Kotlinのnullの安全性を明示的に利用する操作もいくつかあります。たとえば、 List<String?>に対してfilterNotNull()を実行して、List<String>を返すことができます。ここで、すべてのnullが削除されます。

Kotlinで実行できるがJava 8 Streamsでは実行できない実際の追加操作は次のとおりです。

  • zip()およびunzip() – 2つのコレクションを1つのペアのシーケンスに結合するために使用され、逆に、ペアのコレクションを2つのコレクションに変換するために使用されます。

  • associate –コレクションの各エントリを結果のマップのキー/値のペアに変換するラムダを提供することにより、コレクションをマップに変換するために使用されます

例えば:

val numbers = listOf(1, 2, 3)
val words = listOf("one", "two", "three")
numbers.zip(words)

これにより、値が1 to “one”, 2 to “two”および3 to “three”List<Pair<Int, String>>が生成​​されます。

val squares = listOf(1, 2, 3, 4,5)
  .associate { n -> n to n * n }

これにより、Map<Int, Int>が生成​​されます。ここで、キーは1から5までの数値であり、値はそれらの値の2乗です。

6. 概要

Java 8から慣れ親しんだストリーム操作のほとんどは、Kotlinの標準のCollectionクラスで直接使用でき、最初にStreamに変換する必要はありません。

さらに、Kotlinは、使用できる操作と既存の操作のバリエーションを追加することにより、これがどのように機能するかに柔軟性を追加します。

ただし、Kotlinはデフォルトでは熱心であり、遅延ではありません。 使用されているコレクションタイプに注意を払わないと、これにより追加の作業が実行される可能性があります。