What Java 8 Stream.collect equivalents are available in the standard Kotlin library?

后端 未结 4 1782
礼貌的吻别
礼貌的吻别 2020-11-27 09:05

In Java 8, there is Stream.collect which allows aggregations on collections. In Kotlin, this does not exist in the same way, other than maybe as a collection of extension f

4条回答
  •  借酒劲吻你
    2020-11-27 09:21

    For additional examples, here are all the samples from Java 8 Stream Tutorial converted to Kotlin. The title of each example, is derived from the source article:

    How streams work

    // Java:
    List 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)
    

    Different Kinds of Streams #1

    // Java:
    Arrays.asList("a1", "a2", "a3")
        .stream()
        .findFirst()
        .ifPresent(System.out::println);    
    
    // Kotlin:
    listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
    

    or, create an extension function on String called 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)
    

    See also: apply() function

    See also: Extension Functions

    See also: ?. Safe Call operator, and in general nullability: In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them

    Different Kinds of Streams #2

    // Java:
    Stream.of("a1", "a2", "a3")
        .findFirst()
        .ifPresent(System.out::println);    
    
    // Kotlin:
    sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
    

    Different Kinds of Streams #3

    // Java:
    IntStream.range(1, 4).forEach(System.out::println);
    
    // Kotlin:  (inclusive range)
    (1..3).forEach(::println)
    

    Different Kinds of Streams #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)
    

    Different Kinds of Streams #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)
    

    Different Kinds of Streams #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)
    

    Different Kinds of Streams #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)
    

    Why Order Matters

    This section of the Java 8 Stream Tutorial is the same for Kotlin and Java.

    Reusing Streams

    In Kotlin, it depends on the type of collection whether it can be consumed more than once. A Sequence generates a new iterator every time, and unless it asserts "use only once" it can reset to the start each time it is acted upon. Therefore while the following fails in Java 8 stream, but works in Kotlin:

    // Java:
    Stream 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
    

    And in Java to get the same behavior:

    // Java:
    Supplier> 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
    

    Therefore in Kotlin the provider of the data decides if it can reset back and provide a new iterator or not. But if you want to intentionally constrain a Sequence to one time iteration, you can use constrainOnce() function for Sequence as follows:

    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. 
    

    Advanced Operations

    Collect example #5 (yes, I skipped those already in the other answer)

    // 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.
    

    And as a side note, in Kotlin we can create simple data classes and instantiate the test data as follows:

    // 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))
    

    Collect example #6

    // Java:
    Map 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, a more interest case here for Kotlin. First the wrong answers to explore variations of creating a Map from a collection/sequence:

    // 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> instead of Map
    
    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> instead of Map
    

    And now for the correct answer:

    // 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!!
    

    We just needed to join the matching values to collapse the lists and provide a transformer to jointToString to move from Person instance to the Person.name.

    Collect example #7

    Ok, this one can easily be done without a custom Collector, so let's solve it the Kotlin way, then contrive a new example that shows how to do a similar process for Collector.summarizingInt which does not natively exist in Kotlin.

    // Java:
    Collector 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(" | ")
    

    It's not my fault they picked a trivial example!!! Ok, here is a new summarizingInt method for Kotlin and a matching sample:

    SummarizingInt Example

    // 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)
    

    But it is better to create an extension function, 2 actually to match styles in Kotlin stdlib:

    // Kotlin:
    inline fun Collection.summarizingInt(): SummaryStatisticsInt
            = this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }
    
    inline fun  Collection.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
            this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }
    

    Now you have two ways to use the new summarizingInt functions:

    val stats2 = persons.map { it.age }.summarizingInt()
    
    // or
    
    val stats3 = persons.summarizingInt { it.age }
    

    And all of these produce the same results. We can also create this extension to work on Sequence and for appropriate primitive types.

    For fun, compare the Java JDK code vs. Kotlin custom code required to implement this summarization.

提交回复
热议问题