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

181

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

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

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

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

С подробностями в JavaDoc связаны выше.

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

Джейсон Минард
источник
В случаях, когда у вас нет выбора, кроме как использовать collect(Collectors.toList())или подобное, вы можете решить эту проблему: stackoverflow.com/a/35722167/3679676 (проблема, с обходными путями)
Jayson Minard

Ответы:

257

В Kotlin stdlib есть функции для усреднения, подсчета, различения, фильтрации, поиска, группировки, объединения, отображения, минимального, максимального, разбиения, нарезки, сортировки, суммирования, списков в / из массивов, списков в / из карт, карт в / из карт , объединение, ко-итерация, все функциональные парадигмы и многое другое. Таким образом, вы можете использовать их для создания маленьких однострочников, и нет необходимости использовать более сложный синтаксис Java 8.

Я думаю, что единственное, чего не хватает во встроенном Collectorsклассе Java 8, это суммирование (но в другом ответе на этот вопрос простое решение) .

Одна вещь, отсутствующая в обоих, - это пакетирование по количеству, которое видно в другом ответе переполнения стека и также имеет простой ответ. Еще один интересный случай - это также из Stack Overflow: идиоматический способ разделения последовательности на три списка с использованием Kotlin . А если вы хотите создать что-то подобное Stream.collectдля другой цели, см. Custom Stream.collect в Kotlin

РЕДАКТИРОВАТЬ 11.08.2017: Операции сбора по частям / окнам были добавлены в kotlin 1.2 M2, см. Https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Всегда полезно изучить Справочник по API для kotlin.collections в целом, прежде чем создавать новые функции, которые могут там уже существовать.

Вот некоторые преобразования из Stream.collectпримеров Java 8 в эквивалент в 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()перед цепочкой. В конце цепочки функций, вы обычно также заканчиваете с 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, потому что он также имеет обнуляемые типы и может помочь предотвратить страшный NPE. Итак, это в Котлине:

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

такой же как:

val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }

Поскольку котлинский знает , что peopleесть, и что people.ageэто , Intследовательно , выражение фильтра допускает только сравнение к Int, и что people.nameэто , Stringследовательно, mapшаг производит List<String>(только для чтения Listиз String).

Теперь, если бы peopleбыло возможно null, как-то List<People>?тогда:

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

Возвращает a List<String>?, который должен быть проверен на нуль ( или использовать один из других операторов Kotlin для значений Nullable, см. Этот идиоматический способ Kotlin для работы со значениями Nullable, а также Idiomatic способ обработки пустых или пустых списков в Kotlin )

Смотрите также:

Джейсон Минард
источник
Есть ли в Kotlin эквивалент JavaStrallelStream ()?
Арнаб
Ответ о неизменяемых коллекциях и Kotlin - это тот же ответ для @arnab здесь для параллели, существуют другие библиотеки, используйте их: stackoverflow.com/a/34476880/3679676
Джейсон Минард,
2
@arnab Возможно, вы захотите взглянуть на поддержку Kotlin для функций Java 7/8 (в частности, kotlinx-support-jdk8), которая была доступна ранее в этом году: обсуждения.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
роботация
Действительно ли идиоматично использовать 3 разных «это» ссылки в одном утверждении?
herman
2
Это предпочтение, в приведенных выше примерах я держал их краткими и предоставлял только локальное имя для параметра, если это необходимо.
Джейсон Минард
47

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

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

См. Также: ?.Оператор безопасного вызова и вообще обнуляемость: в 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 одинаков для Kotlin и Java.

Повторное использование потоков

В Kotlin, это зависит от типа коллекции, может ли она быть использована более одного раза. A 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

Поэтому в Котлине провайдер данных решает, сможет ли он сбросить данные и предоставить новый итератор или нет. Но если вы хотите преднамеренно ограничить 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, необходимым для реализации этого обобщения.

Джейсон Минард
источник
В потоке 5 нет плюса использовать две карты вместо одной .map { it.substring(1).toInt() }: как вы знаете, выводимый тип - это сила котлина.
Мишель д'Амико
правда, но нет и недостатков (для сравнения я держал их отдельно)
Джейсон Минард
Но код Java можно легко сделать параллельным, поэтому во многих случаях лучше вызывать код потока Java из Kotlin.
Говард Ловатт
@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можете столкнуться с ситуацией, когда вам это может не понадобиться, и эти расширения помогут вам вернуться к стандартным коллекциям и API Kotlin.

Джейсон Минард
источник
2

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

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

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 }
Герман
источник