it-roy-ru.com

Какие Java 8 эквиваленты Stream.collect доступны в стандартной библиотеке Kotlin?

В Java 8 есть Stream.collect , который разрешает агрегацию в коллекциях. В Kotlin, это не существует таким же образом, за исключением, может быть, как набор функций расширения в stdlib. Но не ясно, каковы эквивалентности для разных вариантов использования.

Например, в в верхней части JavaDoc для Collectors приведены примеры, написанные для Java 8, и при переносе их на Kolin вы не можете использовать Java 8 классов в другой версии JDK, поэтому, скорее всего, они должны быть написаны по-разному.

С точки зрения ресурсов онлайн, показывающих примеры коллекций Kotlin, они, как правило, тривиальны и на самом деле не сравниваются с теми же вариантами использования. Каковы хорошие примеры, которые действительно соответствуют случаям, таким как документированные для Java 8 Stream.collect? Список там есть:

  • Накапливать имена в список
  • Накапливать имена в TreeSet
  • Преобразуйте элементы в строки и объедините их через запятую
  • Рассчитать сумму заработной платы работника
  • Сотрудники группы по отделам
  • Рассчитать сумму зарплат по отделам
  • Разделить студентов на прохождение и провал

С деталями в JavaDoc связаны выше.

Примечание: этот вопрос намеренно написан и дан ответ автором ( Вопросы с самоотвечением ), так что идиоматические ответы на часто задаваемые темы Kotlin присутствуют в SO. Также, чтобы прояснить некоторые действительно старые ответы, написанные для альф Kotlin, которые не являются точными для сегодняшнего Kotlin.

158
Jayson Minard

Для дополнительных примеров, вот все примеры из 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)

или создайте в String функцию расширения с именем 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

Смотрите также: Функции расширения

См. Также: ?. Оператор безопасного вызова и вообще обнуляемость: В Kotlin, каков идиоматический способ работы со значениями, допускающими обнуляемость, обращаясь к ним или преобразовывая их

Различные виды потоков # 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 Stream Tutorial одинаков для 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 однократной итерацией, вы можете использовать функцию constrainOnce() для Sequence следующим образом:

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}    

Хорошо, более интересный случай здесь для Котлина. Сначала неправильные ответы, чтобы исследовать варианты создания 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, а затем создадим новый пример, который показывает, как выполнить аналогичный процесс для Collector.summarizingInt, который изначально не существует в Kotlin.

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

Я не виноват, что они выбрали тривиальный пример !!! Хорошо, вот новый метод summarizingInt для Kotlin и соответствующий пример:

Пример 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:

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

// or

val stats3 = persons.summarizingInt { it.age }

И все они дают одинаковые результаты. Мы также можем создать это расширение для работы с Sequence и для соответствующих типов примитивов.

Для забавы сравните код Java JDK с пользовательским кодом Kotlin , необходимый для реализации этого обобщения.

43
Jayson Minard

В некоторых случаях трудно избежать вызова 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() вернуться в API Kotlin. Случай, такой как Files.list(path), вынуждает вас ввести Stream, когда вы, возможно, этого не хотите, и эти расширения могут помочь вам вернуться к стандартным коллекциям и API Kotlin.

3
Jayson Minard

Больше на лень

Давайте возьмем пример решения для "Расчет суммы зарплат по отделам", данное Джейсоном:

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 }
2
herman