Java 8には、コレクションでの集約を許可する Stream.collect
があります。 Kotlinでは、これはstdlibの拡張機能のコレクションとしての場合を除き、同じようには存在しません。しかし、異なるユースケースの等価性が明確ではありません。
たとえば、 Collectors
のJavaDocの先頭 にはJava 8の例があります。Kolinに移植する場合、Javaは使用できません。異なるJDKバージョンを使用している場合は8つのクラスであるため、異なる方法で記述する必要があります。
Kotlinコレクションの例を示すオンラインリソースの観点から見ると、それらは一般に些細なものであり、同じユースケースと実際には比較されません。 Java 8 Stream.collect
について文書化されているようなケースに本当に一致する良い例は何ですか?リストは次のとおりです。
上記のJavaDocに詳細が記載されています。
注:この質問は作者によって意図的に記述され回答されます( Self-Answered Questions )。これにより、よく聞かれるKotlinのトピックに対する慣用的な回答がSOに存在します。また、現在のコトリンでは正確ではないコトリンのアルファ用に書かれたいくつかの本当に古い回答を明確にするために。
Kotlin stdlibには、平均、カウント、個別、フィルタリング、検索、グループ化、結合、マッピング、最小、最大、パーティショニング、スライス、ソート、合計、配列への/からのリストへ、リストから/への関数があります、ユニオン、共同反復、すべての機能的パラダイムなど。したがって、これらを使用して小さな1ライナーを作成でき、Java 8のより複雑な構文を使用する必要はありません。
組み込みのJava 8 Collectors
クラスに欠けているのは要約だけだと思います(しかし この質問に対する別の答え は簡単な解決策です) 。
両方に欠けているものの1つは、カウントによるバッチ処理です。これは 別のスタックオーバーフローの答え で見られ、簡単な答えもあります。もう1つの興味深いケースは、これもStack Overflowの例です。 Kotlinを使用してシーケンスを3つのリストに流し込む慣用的な方法 。また、別の目的でStream.collect
のようなものを作成する場合は、 KotlinのカスタムStream.collect を参照してください
EDIT 11.08.2017:kotlin 1.2 M2でチャンク/ウィンドウ化されたコレクション操作が追加されました https://blog.jetbrains.com/ kotlin/2017/08/kotlin-1-2-m2-is-out /
既に存在する可能性のある新しい関数を作成する前に、 kotlin.collectionsのAPIリファレンス 全体を検討することは常に有効です。
Java 8 Stream.collect
の例からKotlinの同等のものへの変換を次に示します。
リストに名前を蓄積する
// Java:
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name } // toList() not needed
要素を文字列に変換し、コンマで区切って連結します
// Java:
String joined = things.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString(", ")
従業員の給与の合計を計算する
// Java:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }
部門ごとにグループ従業員
// Java:
Map<Department, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }
部門ごとの給与の合計を計算する
// Java:
Map<Department, Integer> totalByDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
合格と不合格に学生を分割する
// Java:
Map<Boolean, List<Student>> passingFailing =
students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
// Kotlin:
val passingFailing = students.partition { it.grade >= PASS_THRESHOLD }
男性メンバーの名前
// Java:
List<String> namesOfMaleMembers = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
// Kotlin:
val namesOfMaleMembers = roster.filter { it.gender == Person.Sex.MALE }.map { it.name }
名簿のメンバーの性別によるグループ名
// Java:
Map<Person.Sex, List<String>> namesByGender =
roster.stream().collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
// Kotlin:
val namesByGender = roster.groupBy { it.gender }.mapValues { it.value.map { it.name } }
リストを別のリストにフィルターする
// Java:
List<String> filtered = items.stream()
.filter( item -> item.startsWith("o") )
.collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { it.startsWith('o') }
最短文字列のリストを見つける
// Java:
String shortest = items.stream()
.min(Comparator.comparing(item -> item.length()))
.get();
// Kotlin:
val shortest = items.minBy { it.length }
フィルター適用後のリスト内のアイテムのカウント
// Java:
long count = items.stream().filter( item -> item.startsWith("t")).count();
// Kotlin:
val count = items.filter { it.startsWith('t') }.size
// but better to not filter, but count with a predicate
val count = items.count { it.startsWith('t') }
そしてそれは...すべての場合において、Stream.collect
を模倣するために特別なfold、reduce、または他の機能は必要ありませんでした。さらにユースケースがある場合は、コメントに追加してください。
チェーンを遅延処理したい場合は、チェーンの前でasSequence()
を使用してSequence
に変換できます。関数のチェーンの最後に、通常、Sequence
も含まれます。その後、toList()
、toSet()
、toMap()
またはその他の関数を使用して、最後にSequence
を具体化できます。
// switch to and from lazy
val someList = items.asSequence().filter { ... }.take(10).map { ... }.toList()
// switch to lazy, but sorted() brings us out again at the end
val someList = items.asSequence().filter { ... }.take(10).map { ... }.sorted()
Kotlinの例ではタイプが指定されていないことに気付くでしょう。これは、Kotlinには完全な型推論があり、コンパイル時に完全に型安全であるためです。これはJavaよりも重要です。これには、null許容型もあり、恐ろしいNPEの防止に役立つからです。 Kotlinの場合:
val someList = people.filter { it.age <= 30 }.map { it.name }
次と同じです:
val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }
Kotlinはpeople
が何であるか、people.age
がInt
であることを知っているため、フィルター式ではInt
との比較のみが許可され、people.name
はString
であるため、map
ステップはList
のList<String>
(読み取り専用String
)を生成します。
ここで、people
がnull
である可能性がある場合、List<People>?
のように:
val someList = people?.filter { it.age <= 30 }?.map { it.name }
Nullチェックが必要なList<String>?
を返します(またはnull値を許可するために他のKotlin演算子の1つを使用します。これを参照してください Kotlinのnull値を扱う慣用的な方法 および- Kotlinでヌル可能または空のリストを処理する慣用的な方法)
追加の例として、 Java 8 Stream Tutorial がKotlinに変換されたすべてのサンプルを以下に示します。各例のタイトルは、ソースの記事から派生しています。
ストリームの仕組み
// Java:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2
// Kotlin:
val list = listOf("a1", "a2", "b1", "c2", "c1")
list.filter { it.startsWith('c') }.map (String::toUpperCase).sorted()
.forEach (::println)
異なる種類のストリーム#1
// Java:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
または、ifPresentという名前のストリングに拡張関数を作成します。
// Kotlin:
inline fun String?.ifPresent(thenDo: (String)->Unit) = this?.apply { thenDo(this) }
// now use the new extension function:
listOf("a1", "a2", "a3").firstOrNull().ifPresent(::println)
参照: apply()
function
参照: 拡張機能
参照: ?.
Safe Call operator 、および一般的なnullability: Kotlinでは、null許容値を処理、参照または変換する慣用的な方法は何ですか
異なる種類のストリーム#2
// Java:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
異なる種類のストリーム#3
// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin: (inclusive range)
(1..3).forEach(::println)
異なる種類のストリーム#4
// Java:
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
// Kotlin:
arrayOf(1,2,3).map { 2 * it + 1}.average().apply(::println)
異なる種類のストリーム#5
// Java:
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
// Kotlin:
sequenceOf("a1", "a2", "a3")
.map { it.substring(1) }
.map(String::toInt)
.max().apply(::println)
異なる種類のストリーム#6
// Java:
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin: (inclusive range)
(1..3).map { "a$it" }.forEach(::println)
ストリームの種類#7
// Java:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
// Kotlin:
sequenceOf(1.0, 2.0, 3.0).map(Double::toInt).map { "a$it" }.forEach(::println)
Java 8ストリームチュートリアルのこのセクションは、KotlinとJavaで同じです。
Kotlinでは、コレクションのタイプによって、それが複数回使用できるかどうかに依存します。 Sequence
は毎回新しいイテレータを生成し、「一度だけ使用」をアサートしない限り、実行されるたびに開始にリセットできます。したがって、以下はJava 8ストリームでは失敗しますが、Kotlinでは機能します:
// Java:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("b"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
// Kotlin:
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
stream.forEach(::println) // b1, b2
println("Any B ${stream.any { it.startsWith('b') }}") // Any B true
println("Any C ${stream.any { it.startsWith('c') }}") // Any C false
stream.forEach(::println) // b1, b2
Javaで同じ動作を取得するには:
// Java:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
したがって、Kotlinでは、データのプロバイダーがリセットして新しいイテレーターを提供できるかどうかを決定します。しかし、意図的にSequence
を1回の繰り返しに制限する場合は、次のようにSequence
に対してconstrainOnce()
関数を使用できます。
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
.constrainOnce()
stream.forEach(::println) // b1, b2
stream.forEach(::println) // Error:Java.lang.IllegalStateException: This sequence can be consumed only once.
例5を収集する(はい、他の回答で既にスキップしたもの)
// Java:
String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.
// Kotlin:
val phrase = persons.filter { it.age >= 18 }.map { it.name }
.joinToString(" and ", "In Germany ", " are of legal age.")
println(phrase)
// In Germany Max and Peter and Pamela are of legal age.
補足として、Kotlinでは、単純な データクラス を作成し、次のようにテストデータをインスタンス化できます。
// Kotlin:
// data class has equals, hashcode, toString, and copy methods automagically
data class Person(val name: String, val age: Int)
val persons = listOf(Person("Tod", 5), Person("Max", 33),
Person("Frank", 13), Person("Peter", 80),
Person("Pamela", 18))
例6を収集する
// Java:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));
System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}
OK、コトリンのより興味深い事例です。最初に、コレクション/シーケンスからMap
を作成するバリエーションを探る間違った答え:
// Kotlin:
val map1 = persons.map { it.age to it.name }.toMap()
println(map1)
// output: {18=Max, 23=Pamela, 12=David}
// Result: duplicates overridden, no exception similar to Java 8
val map2 = persons.toMap({ it.age }, { it.name })
println(map2)
// output: {18=Max, 23=Pamela, 12=David}
// Result: same as above, more verbose, duplicates overridden
val map3 = persons.toMapBy { it.age }
println(map3)
// output: {18=Person(name=Max, age=18), 23=Person(name=Pamela, age=23), 12=Person(name=David, age=12)}
// Result: duplicates overridden again
val map4 = persons.groupBy { it.age }
println(map4)
// output: {18=[Person(name=Max, age=18)], 23=[Person(name=Peter, age=23), Person(name=Pamela, age=23)], 12=[Person(name=David, age=12)]}
// Result: closer, but now have a Map<Int, List<Person>> instead of Map<Int, String>
val map5 = persons.groupBy { it.age }.mapValues { it.value.map { it.name } }
println(map5)
// output: {18=[Max], 23=[Peter, Pamela], 12=[David]}
// Result: closer, but now have a Map<Int, List<String>> instead of Map<Int, String>
そして今、正しい答えのために:
// Kotlin:
val map6 = persons.groupBy { it.age }.mapValues { it.value.joinToString(";") { it.name } }
println(map6)
// output: {18=Max, 23=Peter;Pamela, 12=David}
// Result: YAY!!
一致する値を結合してリストを折りたたみ、トランスフォーマーをjointToString
に提供して、Person
インスタンスからPerson.name
に移動する必要がありました。
例7を収集する
わかりました、これはカスタムCollector
なしで簡単に実行できるので、それをKotlinの方法で解決してから、Kotlinに本来存在しないCollector.summarizingInt
に対して同様のプロセスを実行する方法を示す新しい例を考えてみましょう。
// Java:
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher
String names = persons
.stream()
.collect(personNameCollector);
System.out.println(names); // MAX | PETER | PAMELA | DAVID
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")
些細な例を選んだのは私のせいではありません!!!OK、コトリンの新しいsummarizingInt
メソッドとそれに対応するサンプルがあります:
SummarizingIntの例
// Java:
IntSummaryStatistics ageSummary =
persons.stream()
.collect(Collectors.summarizingInt(p -> p.age));
System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
// Kotlin:
// something to hold the stats...
data class SummaryStatisticsInt(var count: Int = 0,
var sum: Int = 0,
var min: Int = Int.MAX_VALUE,
var max: Int = Int.MIN_VALUE,
var avg: Double = 0.0) {
fun accumulate(newInt: Int): SummaryStatisticsInt {
count++
sum += newInt
min = min.coerceAtMost(newInt)
max = max.coerceAtLeast(newInt)
avg = sum.toDouble() / count
return this
}
}
// Now manually doing a fold, since Stream.collect is really just a fold
val stats = persons.fold(SummaryStatisticsInt()) { stats, person -> stats.accumulate(person.age) }
println(stats)
// output: SummaryStatisticsInt(count=4, sum=76, min=12, max=23, avg=19.0)
しかし、実際にはKotlin stdlibのスタイルに合わせて拡張機能2を作成することをお勧めします。
// Kotlin:
inline fun Collection<Int>.summarizingInt(): SummaryStatisticsInt
= this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }
inline fun <T: Any> Collection<T>.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }
これで、新しいsummarizingInt
関数を使用する2つの方法があります。
val stats2 = persons.map { it.age }.summarizingInt()
// or
val stats3 = persons.summarizingInt { it.age }
そして、これらはすべて同じ結果をもたらします。また、この拡張機能を作成して、Sequence
および適切なプリミティブ型で機能するようにできます。
楽しみのために、この要約を実装するには Java JDKコードとKotlinカスタムコードを比較してください が必要です。
collect(Collectors.toList())
などの呼び出しを避けるのが難しい場合があります。これらの場合、次のような拡張機能を使用して、Kotlinの同等の機能にすばやく変更できます。
fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())
fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()
その後、単にstream.toList()
またはstream.asSequence()
を使用してKotlin APIに戻ることができます。 Files.list(path)
などのケースでは、必要ない場合はStream
に強制されます。これらの拡張機能を使用すると、標準のコレクションとKotlin APIに戻すことができます。
Jaysonが提供した「部門別の給与の合計を計算する」ためのソリューション例を見てみましょう。
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}
これを遅延させる(つまり、groupBy
ステップで中間マップを作成しないようにする)ために、asSequence()
を使用することはできません。代わりに、groupingBy
およびfold
操作を使用する必要があります。
val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }
一部の人にとっては、マップエントリを扱っていないため、さらに読みやすいかもしれません。ソリューションのit.value
の部分は、最初は私にとっても混乱を招きました。
これは一般的なケースであり、毎回fold
を書き出したくないので、汎用のsumBy
関数をGrouping
に提供する方が良いかもしれません:
public inline fun <T, K> Grouping<T, K>.sumBy(
selector: (T) -> Int
): Map<K, Int> =
fold(0) { acc, element -> acc + selector(element) }
次のように書くことができます。
val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }