KotlinのJava 8 Stream APIアナロジー
1前書き
Java 8では、
Streams
の概念がコレクション階層に導入されました。
これらはプロセスを機能させるためにいくつかの関数型プログラミングの概念を利用して、非常に読みやすい方法でデータのいくつかの非常に強力な処理を可能にします。
Kotlinの慣用句を使用して、どのようにして同じ機能を実現できるかを調べます。普通のJavaでは利用できない機能についても見ていきます。
2 JavaとKotlin
Java 8では、新しいファンシーAPIは
java.util.stream.Stream
インスタンスと対話するときにのみ使用できます。
良いことは、すべての標準コレクション(
java.util.Collection
を実装するものすべて)に、
Stream
インスタンスを生成できる特定のメソッド
stream()
があることです。
Stream
は
Collection.
ではないことを覚えておくことは重要です。** これは
java.util.Collection
を実装していないし、Javaの
Collections
の通常のセマンティクスも実装していません。それは
Collection
から派生したものであり、それを処理するために使用され、表示されている各要素に対して操作を実行します。
-
Kotlinでは、最初に変換する必要なしに、すべてのコレクション型が既にこれらの操作をサポートしています。コレクションのセマンティクスが間違っている場合にのみ変換が必要になります。たとえば、
Set
には一意の要素がありますが、順序は異なります。
この利点の1つは、
collect()呼び出しを使用して、
Collection
から
Streamへの初期変換、および
Stream
からコレクションへの最終変換が不要なことです。
たとえば、Java 8では、次のように書く必要があります。
someList
.stream()
.map()//some operations
.collect(Collectors.toList());
Kotlinの同等物は非常に簡単です:
someList
.map()//some operations
-
さらに、Java 8
Streams
も再利用できません。**
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バージョンでは
map()
と
filter()
を1つだけ実行します。
-
Kotlinのすべてのコレクションは、
asSequence()
メソッドを使用して遅延シーケンスに変換できます。
上記の例で
List
の代わりに
Sequence
を使用すると、Java 8と同じ数の操作が実行されます。
4 Java 8 __ストリーム操作
Java 8では、
Stream
操作は2つのカテゴリに分類されます。
-
中間と
-
ターミナル
中間操作は、基本的にある
Stream
を別の遅延に変換します。たとえば、すべての整数の
Stream
をすべての偶数の整数の
Stream
に変換します。
端末オプションは
Stream
メソッドチェーンの最後のステップであり、実際の処理を開始します。
コトリンではそのような区別はありません。代わりに、これらはすべて、コレクションを入力として受け取り、新しい出力を生成する単なる関数です。
Kotlinで熱心なコレクションを使用している場合、これらの操作はすぐに評価されます。Javaと比較すると驚くかもしれません。
遅延する必要がある場合は、最初に
Sequence
に変換することを忘れないでください。
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」 – 32を返します。
-
これらの操作の中には、新しいバージョンを生成するのではなく、提供されたコレクションに出力する追加のバージョン(接尾辞
“ To”
** が付いたもの)もあります。
これは、いくつかの入力コレクションを同じ出力コレクションに処理するのに役立ちます。次に例を示します。
val target = mutableList<Int>()
listOf(1, 2, 3, 4, 5)
.filterTo(target) { n -> n % 2 == 0 }
これにより、値「2」と「4」がリスト「target」に挿入されます。
-
直接置換を通常行わない唯一の操作は
peek()
** – フローを中断せずに処理パイプラインの途中で
Stream
内のエントリを反復するためにJava 8で使用されます。
熱心なコレクションの代わりにlazy
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" }
これにより、「Triangular number 3:6」の形式で、最初の5つの三角形の数が生成されます。
もう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. ターミナル操作
-
Java 8 Streams APIのすべての標準端末操作は、
collect
を除いて、Kotlinで直接置き換えられます。
それらのいくつかは異なる名前を持っています:
-
anyMatch()
– >
any()
-
allMatch()
– >
all()
-
noneMatch()
– >
none()
そのうちのいくつかはKotlinがどのように違うかを扱うための追加のバリエーションを持っています –
first()
と
firstOrNull()
があります。
興味深いケースは
collect
です。 Java 8はこれを使って、提供された戦略を使って
Stream
要素をすべてあるコレクションに集めることができます。
これは任意の
Collector
を提供することを可能にし、それはコレクション内のすべての要素と共に提供され、そしてある種の出力を生成します。これらは
Collectors
ヘルパークラスから使用されますが、必要に応じて独自のものを書くことができます。
-
Kotlinでは、コレクションオブジェクト自体のメンバとして直接利用可能なほとんどすべての標準的なコレクタに直接置き換わるものがあります** – 提供されているコレクタに追加のステップは必要ありません。
ここでの1つの例外は
summarizingDouble
/
summizingInt
/
summizingLong
メソッドです。これらは、平均、カウント、最小、最大、合計をまとめて生成します。これらはそれぞれ個別に製造することができます – それは明らかに高いコストを持っていますが。
別の方法として、for-eachループを使用して管理し、必要に応じて手動で処理することができます。
5 Kotlinでの追加操作
Kotlinはコレクションを追加した操作を追加していますが、Java 8ではこれを実装しなければ不可能です。
上記のように、これらのいくつかは単に標準操作の拡張です。たとえば、新しいコレクションを返すのではなく、既存のコレクションに結果が追加されるように、すべての操作を実行することができます。
多くの場合、問題の要素だけでなく要素のインデックスもラムダに提供することも可能です – 順序付けられたコレクションの場合、インデックスは意味があります。
Kotlinの無効性を明示的に利用する操作もいくつかあります。
List <String?>
に対して
filterNotNull()
を実行して、すべてのnullが削除された
List <String>
を返すことができます。
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から “one”、2から “two”
、および
3から “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から慣れ親しんだストリーム操作のほとんどは、標準のCollectionクラスのKotlinで直接使用でき、最初は
Stream
に変換する必要はありません。
さらに、Kotlinは、使用可能な操作を追加したり、既存の操作にバリエーションを追加したりすることで、これがどのように機能するかをより柔軟にします。
しかし、Kotlinはデフォルトで熱心で、怠け者ではありません。使用されているコレクションの種類に注意を払わないと、追加の作業が行われる可能性があります。