1. 序章

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

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

2. Javaと Kotlin

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

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

Streamコレクションではないことを覚えておくことが重要です。java.util.Collectionを実装せず、Javaのコレクションの通常のセマンティクスも実装しません。 。Collection から派生し、それを処理するために使用され、表示される各要素に対して操作を実行するという点で、1回限りのIteratorに似ています。 。

Kotlinでは、すべてのコレクションタイプがこれらの操作をすでにサポートしています。最初に変換する必要はありません。 変換が必要になるのは、コレクションのセマンティクスが間違っている場合のみです。たとえば、 Set には一意の要素がありますが、順序付けされていません。

この利点の1つは、コレクションからストリームへの最初の変換が不要であり、ストリームからへの最終変換が不要なことです。コレクション– collect()呼び出しを使用します。

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

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

Kotlinでの同等の機能は非常に単純です。

someList
  .map() // some operations

さらに、Java8ストリームも再利用できません。 Stream が消費された後は、再度使用することはできません。

たとえば、次は機能しません。

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

Kotlinでは、これらがすべて通常のコレクションであるという事実は、この問題が発生しないことを意味します。 中間状態を変数に割り当ててすばやく共有することができ、期待どおりに機能します。

3. 怠惰なシーケンス

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

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

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

Kotlinでは、コレクションは怠惰ではなく熱心です。 ここでの例外は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()のみを実行します。

Kotlinのすべてのコレクションは、asSequence()メソッドを使用してレイジーシーケンスに変換できます。

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

4. Java8ストリーム操作

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

  • 中級および
  • ターミナル

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

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

Kotlinにはそのような区別はありません。 代わりに、これらはすべて、コレクションを入力として受け取り、新しい出力を生成する単なる関数です。

Kotlinで熱心なコレクションを使用している場合、これらの操作はすぐに評価されることに注意してください。これは、Javaと比較すると驚くべきことです。 怠惰にする必要がある場合は、最初にシーケンスに変換することを忘れないでください。

4.1. 中間操作

Java 8 Streams APIからのほとんどすべての中間操作には、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²が返されます。

これらの操作の一部には、新しいコレクションを作成する代わりに、提供されたコレクションに出力する「To」という単語が接尾辞として付いた追加バージョンもあります。

これは、複数の入力コレクションを同じ出力コレクションに処理する場合に役立ちます。次に例を示します。

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

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

通常は直接置換されない唯一の操作はpeek()です。Java8で使用され、処理パイプラインの途中でStreamのエントリを中断せずに反復処理します。フロー。

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

標準の中間操作には、作業を楽にするいくつかの追加のバリエーションもあります。 たとえば、 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<Char>
  .filter { c -> Character.isUpperCase(c) }

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

4.2. ターミナルオペレーション

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

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

  • anyMatch()-> any()
  • allMatch()-> all()
  • noneMatch()-> none()

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

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

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

Kotlinでは、コレクションオブジェクト自体のメンバーとして直接利用できるほとんどすべての標準コレクターを直接置き換えることができます –コレクターが提供されている場合は追加の手順は必要ありません。

ここでの唯一の例外は、 summaryDouble / summaryizingInt / summaryLong メソッドです。これは、平均、カウント、最小、最大、合計を一度に生成します。 これらはそれぞれ個別に製造できますが、明らかにコストが高くなります。

または、for-eachループを使用して管理し、必要に応じて手動で処理することもできます。これらの5つの値すべてが同時に必要になる可能性は低いため、重要な値のみを実装する必要があります。 。

5. Kotlinでの追加操作

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

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

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

Kotlinのnullの安全性を明示的に利用する操作もいくつかあります。たとえば、 私たちは実行することができます filterNotNull()リストを返すにはリスト 、すべてのnullが削除されます。

Kotlinで実行できるが、Java8ストリームでは実行できない実際の追加操作には次のものがあります。

  • zip()および unzip() – 2つのコレクションを1つのペアのシーケンスに結合するために使用され、逆に、ペアのコレクションを2つのコレクションに変換するために使用されます
  • associate –コレクション内の各エントリを結果のマップ内のキー/値ペアに変換するラムダを提供することにより、コレクションをマップに変換するために使用されます

例えば:

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

これにより、 リスト >> 、値付き 1から「1」、2から「2」 3から「3」

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

これにより、 地図 、ここで、キーは1から5までの数字であり、値はそれらの値の2乗です。

6. 概要

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

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

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