Avatar ·

Какие аналоги Java 8 Stream.collect существуют в стандартной библиотеке Kotlin?

Привет! Я перехожу с Java на Kotlin и активно изучаю стандартную библиотеку. В Java я часто использовал Stream API, особенно метод collect с различными коллекторами: Collectors.toList(), Collectors.groupingBy(), Collectors.joining() и т.д.

Например, в Java я мог написать:

List<String> names = people.stream()
    .filter(p -> p.getAge() > 18)
    .map(Person::getName)
    .collect(Collectors.toList());

Или сгруппировать людей по возрасту:

Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

В Kotlin я вижу, что есть функции .filter, .map, но не могу найти прямой аналог .collect(Collectors.toList()). Пытался писать .toList(), но не уверен, что это правильно и эффективно.

Подскажите, какие функции в стандартной библиотеке Kotlin заменяют Stream.collect? Особенно интересуют:

  • Сбор в список, множество или другую коллекцию
  • Группировка элементов
  • Разделение на две коллекции по условию
  • Объединение строк
  • Подсчёт и суммирование

Заранее спасибо!

<<>>

Полное руководство по замене Java Stream.collect в Kotlin

В Kotlin стандартная библиотека предоставляет гораздо более элегантные и лаконичные аналоги для всех операций Stream.collect. Вам не нужны отдельные коллекторы — всё реализовано через extension-функции для коллекций и последовательностей.

1. Сбор в список, множество или Map

Вместо .collect(Collectors.toList()) используйте просто .toList():

// Java
List<String> names = people.stream()
    .filter(p -> p.age > 18)
    .map(Person::getName)
    .collect(Collectors.toList());

// Kotlin
val names = people
    .filter { it.age > 18 }
    .map { it.name }
    .toList() // или просто .filter{}.map{} — результат уже List

Важно: в Kotlin большинство функций над коллекциями уже возвращают список, поэтому .toList() часто не нужен. Но если вы работаете с Sequence (ленивые вычисления), .toList() — обязательный терминальный оператор.

Для toSet(), toMap() — аналогично:

val uniqueNames = names.toSet()
val nameToAge = people.associate { it.name to it.age } // Map<String, Int>

2. Группировка (groupingBy)

Java-аналог Collectors.groupingBy() — это функция groupBy:

// Java: Map<Integer, List<Person>> byAge = people.stream().collect(Collectors.groupingBy(Person::getAge));
val byAge: Map<Int, List<Person>> = people.groupBy { it.age }

// Более сложный пример: группировка с преобразованием значений
val namesByAge: Map<Int, List<String>> = people.groupBy({ it.age }, { it.name })

Для более сложной агрегации (например, подсчёт элементов в группе) используйте groupingBy + eachCount:

val countByAge: Map<Int, Int> = people.groupingBy { it.age }.eachCount()

3. Разделение на две коллекции (partitioningBy)

Java Collectors.partitioningBy заменяется на partition:

// Java: Map<Boolean, List<Person>> split = people.stream().collect(Collectors.partitioningBy(p -> p.age > 18));
val (adults, minors) = people.partition { it.age >= 18 }
// adults: List<Person>, minors: List<Person>

4. Объединение строк (joining)

Для Collectors.joining() используйте joinToString():

// Java: String joined = people.stream().map(Person::getName).collect(Collectors.joining(", "));
val joined = people.map { it.name }.joinToString(", ")
// Или ещё проще:
val joined = people.joinToString(", ") { it.name }

5. Агрегация: суммирование, среднее, минимум, максимум

Вместо Collectors.summingInt():

// Java: int totalAge = people.stream().collect(Collectors.summingInt(Person::getAge));
val totalAge = people.sumOf { it.age } // Int
val totalAgeDouble = people.sumOf { it.age.toDouble() } // Double

Для среднего:

val averageAge = people.map { it.age }.average() // Double

Минимум/максимум:

val youngest = people.minByOrNull { it.age }
val oldest = people.maxByOrNull { it.age }

6. Сбор в произвольную коллекцию

Если нужно собрать в специфическую коллекцию (например, LinkedList или TreeSet):

val linkedList = listOf(1, 2, 3).toCollection(LinkedList())
val treeSet = listOf(3, 1, 2).toCollection(TreeSet())

7. Сбор с преобразованием (toMap, associate)

Java Collectors.toMap():

// Java: Map<String, Integer> nameToAge = people.stream().collect(Collectors.toMap(Person::getName, Person::getAge));
val nameToAge = people.associate { it.name to it.age }
// Или с обработкой дубликатов:
val nameToAgeSafe = people.associateBy { it.name } // если ключи уникальны

Важное замечание

В Kotlin большинство операций над коллекциями выполняются жадно (eagerly), в отличие от Java Stream, который ленивый. Если вам нужна ленивая обработка (как в Java Stream), используйте Sequence:

val result = people.asSequence()
    .filter { it.age > 18 }
    .map { it.name }
    .toList() // терминальная операция

Таким образом, в Kotlin вам не нужен отдельный класс Collectors — все операции встроены прямо в коллекции и последовательности. Код становится короче и читаемее.

<<>>

Альтернативный подход: использование mutable-коллекций

Иногда, особенно при работе с большими данными или в циклах, удобнее использовать традиционный императивный подход с изменяемыми коллекциями. В Kotlin это тоже лаконично:

// Вместо сложной цепочки collect
val result = mutableListOf<String>()
for (person in people) {
    if (person.age > 18) {
        result.add(person.name.uppercase())
    }
}

Для группировки с накоплением:

val byAge = mutableMapOf<Int, MutableList<Person>>()
for (person in people) {
    byAge.getOrPut(person.age) { mutableListOf() }.add(person)
}

Этот подход даёт больше контроля и часто работает быстрее при очень больших коллекциях, так как избегает создания промежуточных объектов. Однако в 95% случаев функциональный стиль с filter, map и groupBy предпочтительнее из-за читаемости.

Также помните про buildList и buildMap — они позволяют писать императивный код в функциональном стиле:

val names = buildList {
    for (person in people) {
        if (person.age > 18) add(person.name)
    }
}

Выбирайте подход под конкретную задачу — Kotlin даёт вам свободу!