Какие эквиваленты Java 8 Stream.collect доступны в стандартной библиотеке Kotlin?
В Java 8 есть Stream.collect
который допускает агрегации в коллекциях. В Kotlin, это не существует таким же образом, за исключением, может быть, как набор функций расширения в stdlib. Но не ясно, каковы эквивалентности для разных вариантов использования.
Например, в верхней части JavaDoc for Collectors
приведены примеры, написанные для Java 8, и при портировании их на Kolin вы не можете использовать классы Java 8 в другой версии JDK, поэтому, скорее всего, они должны быть написаны по-другому.
С точки зрения ресурсов онлайн, показывающих примеры коллекций Kotlin, они, как правило, тривиальны и на самом деле не сравниваются с теми же вариантами использования. Каковы хорошие примеры, которые действительно соответствуют случаям, таким как документированные для Java 8 Stream.collect
? Список там есть:
- Накапливать имена в список
- Накапливать имена в TreeSet
- Преобразуйте элементы в строки и объедините их через запятую
- Рассчитать сумму зарплаты работника
- Сотрудники группы по отделам
- Рассчитать сумму зарплат по отделам
- Разделить студентов на прохождение и провал
С деталями в JavaDoc связаны выше.
Примечание: этот вопрос намеренно написан и получен ответ от автора (Вопросы с самоотвечением), так что идиоматические ответы на часто задаваемые котлинские темы присутствуют в SO. Также, чтобы уточнить некоторые действительно старые ответы, написанные для альфа-версий Kotlin, которые не являются точными для сегодняшнего Kotlin.
Ответы
Ответ 1
В Kotlin stdlib есть функции для усреднения, подсчета, различения, фильтрации, поиска, группировки, объединения, отображения, минимального, максимального, разбиения, нарезки, сортировки, суммирования, массивов в/из списков, в/из списков, в/из карт, объединение, ко-итерация, все функциональные парадигмы и многое другое. Таким образом, вы можете использовать их для создания маленьких однострочников, и нет необходимости использовать более сложный синтаксис Java 8.
Я думаю, что единственное, чего не хватает во встроенном классе Java 8 Collectors
- это суммирование (но в другом ответе на этот вопрос есть простое решение).
Одна вещь, отсутствующая в обоих, - это пакетирование по количеству, которое видно в другом ответе и также имеет простой ответ. Еще один интересный случай - это также из 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 в целом, прежде чем создавать новые функции, которые могут там уже существовать.
Вот некоторые преобразования из примеров 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()
перед цепочкой. В конце цепочки функций вы обычно также получаете 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 }
Поскольку Kotlin знает, что такое 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 }
Возвращает List<String>?
это должно быть проверено на нуль (или использовать один из других операторов Kotlin для значений Nullable, см. этот идиоматический способ Kotlin для работы со значениями Nullable, а также Idiomatic способ обработки пустых или пустых списков в Kotlin)
Смотрите также:
Ответ 2
Для дополнительных примеров, вот все примеры из 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 одинаков для 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
метод Котлин и образец соответствия:
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, необходимым для реализации этого обобщения.
Ответ 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()
вернуться в API Kotlin. Случай, например Files.list(path)
, заставляет вас в Stream
, когда вы этого не хотите, и эти расширения могут помочь вам вернуться в стандартные коллекции и API Kotlin.
Ответ 4
Больше на лень
Давайте возьмем пример решения для "Расчет суммы зарплат по отделам", данное 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
в решении поначалу меня тоже 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 }