標準のKotlinライブラリで利用できるJava 8 Stream.collectの同等のものは何ですか?


181

Java 8では、Stream.collectコレクションの集約を可能にするものがあります。Kotlinでは、おそらくstdlibの拡張関数のコレクションとして以外は、これは同じ方法で存在しません。しかし、さまざまなユースケースでの同等性が何であるかは明らかではありません。

たとえば、JavaDocCollectors上部には Java 8用に記述された例があり、それらをKolinに移植する場合、異なるJDKバージョンではJava 8クラスを使用できないため、異なる方法で記述する必要があります。

Kotlinコレクションの例を示すオンラインのリソースに関しては、それらは通常は取るに足らないものであり、実際に同じ使用例と比較することはできません。Java 8用に文書化されているようなケースに本当に一致する良い例は何Stream.collectですか?リストがあります:

  • 名前をリストに蓄積する
  • 名前をTreeSetに蓄積する
  • 要素を文字列に変換し、コンマで区切って連結します
  • 従業員の給与の合計を計算します
  • 部門別のグループ従業員
  • 部門ごとの給与の合計を計算する
  • 生徒を合格と不合格に分ける

上記のリンクのJavaDocの詳細を使用。

注: この質問は、作者が意図的に作成して回答したものであり(自己回答式の質問)、一般的に尋ねられるKotlinトピックに対する慣用的な回答がSOにあります。また、現在のKotlinには正確ではない、Kotlinのアルファ用に書かれたいくつかの本当に古い回答を明確にするためにも。


使用するしか選択肢がない場合、collect(Collectors.toList())または類似のものを使用すると、この問題が発生する可能性があります。stackoverflow.com / a / 35722167/3679676(問題、回避策あり)
Jayson Minard

回答:


257

Kotlin stdlibには、平均、カウント、個別、フィルタリング、検索、グループ化、結合、マッピング、最小、最大、パーティション化、スライス、並べ替え、合計、to / from配列、to / fromリスト、to / fromマップ用の関数があります、労働組合、共同反復、すべての機能的パラダイムなど。したがって、これらを使用して小さな1ライナーを作成でき、Java 8のより複雑な構文を使用する必要はありません。

組み込みのJava 8 Collectorsクラスに欠けているのは要約だけだと思います(ただし、この質問に対する別の答えは簡単な解決策です)

両方に欠けているものの1つは、カウントによるバッチ処理です。これは、別のスタックオーバーフローの回答で見られ、単純な回答もあります。別の興味深いケースは、これもスタックオーバーフローからのものです:Kotlinを使用してシーケンスを3つのリストにスピルする慣用的な方法。またStream.collect、別の目的のために何かを作成したい場合は、KotlinのCustom Stream.collectを参照してください

EDIT 11.08.2017:チャンク/ウィンドウのコレクション操作がkotlin 1.2 M2に追加されました。https: //blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/を参照してください


すでにそこに存在する可能性のある新しい関数を作成する前に、kotlin.collectionsAPIリファレンス全体を探索することは常に良いことです。

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。さらにユースケースがある場合は、コメントに追加してください。

怠惰について

チェーンを遅延処理する場合は、チェーンの前にSequence使用asSequence()に変換できます。一連の関数の最後に、通常はa 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が完全な型推論を持ち、コンパイル時に完全に型安全であるためです。null型もあり、恐ろしいNPEの防止に役立つため、Javaよりも優れています。したがって、これはコトリンで:

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<String>(読み取り専用ListString)。

今、もしpeople可能ならnullList<People>?当時のまま:

val someList = people?.filter { it.age <= 30 }?.map { it.name }

戻り値List<String>?(nullがチェックする必要があるだろうと見、このまたはNULL値可能な値のための他のKotlin演算子のいずれかを使用Kotlin NULL値可能な値に対処するための慣用的な方法ともKotlinにNULL可能か、空のリストを処理する慣用的な方法を

以下も参照してください。


KotlinにJava8のparallelStream()に相当するものはありますか?
arnab 2016年

不変コレクションとKotlinに関する答えは、@ arnabと同じ答えですが、他のライブラリが存在します。それらを使用してください:stackoverflow.com/a/34476880/3679676
Jayson Minard

2
:あなたは今年の初めに利用できるようになりましたJavaの7/8の特徴(特に、kotlinx-サポート-jdk8)用Kotlinのサポートを見たいかもしれません@arnab discuss.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
2016

1つのステートメントで3つの異なる「it」参照を使用することは本当に慣用的ですか?
ヘルマン2018年

2
これは好みです。上のサンプルでは、​​それらを短くして、必要に応じてパラメータのローカル名のみを提供していました。
ジェイソンミナード2018年

47

その他の例については、ここにKotlinに変換されたJava 8 Stream Tutorialのすべてのサンプルがあります。各例のタイトルは、ソース記事から派生しています。

ストリームの仕組み

// 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という名前のStringに拡張関数を作成します。

// 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()関数

参照:拡張関数

参照: ?.安全な呼び出し演算子、および一般的なnull可能性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では、コレクションのタイプに応じて、複数回使用できるかどうかが決まります。A Sequenceは毎回新しいイテレータを生成し、「1度だけ使用」をアサートしない限り、アクションが実行されるたびに最初にリセットできます。したがって、以下は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では、データのプロバイダーがリセットして新しいイテレーターを提供できるかどうかを決定します。ただし、意図的にa Sequenceを1回に制限したい場合constrainOnce()Sequence、次のようにfunctionを使用できます。

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}    

わかりました、Kotlinにとってもっと興味深いケースです。まず、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に移動するためのトランスフォーマーを提供するには、一致する値を結合する必要があるだけです。PersonPerson.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、summarizingIntKotlinの新しいメソッドと一致するサンプルを次に示します。

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)

しかし、拡張関数を作成する方が良いです。実際には、2をKotlin stdlibのスタイルと一致させます。

// 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カスタムコードを比較してください


ストリーム5では、1つのマップの代わりに2つのマップを使用する.map { it.substring(1).toInt() }利点はありません。よく知られているように、推測されたタイプはKotlinの能力の1つです。
Michele d'Amico

本当ですが、欠点もありません(比較のために、それらを個別に保持しました)
Jayson Minard

ただし、Javaコードは簡単に並列化できるため、多くの場合、KotlinからJavaストリームコードを呼び出した方がよいでしょう。
ハワードロバット

@HowardLovattは、並列処理が適切でない場合が多く、特にスレッドプール内にある重いコンカレント環境ではそうです。私は平均的なユースケースが平行していないことを賭けています、そしてそれはまれなケースです。しかし、もちろん、Javaクラスを適切に使用するオプションは常にあり、これは実際にはこの質問と回答の目的ではありませんでした。
ジェイソンミナード

3

通話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に戻すことができます。


2

怠惰の詳細

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 }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.