--- title: Операции description: map, filter, groupBy, fold, zip и остальной арсенал операций над коллекциями tags: - kotlin - collections --- Операции над коллекциями - функции-расширения на `Iterable`. Их около 150; здесь - те, что покрывают почти всю практику. Все они делятся на две группы: - **возвращают коллекцию**: `map`, `filter`, `sorted` - можно строить цепочки - **возвращают одно значение**: `sum`, `first`, `count`, `fold` - завершают цепочку ## Преобразование ```kotlin fun main() { val list = listOf(1, 2, 3) println(list.map { it * 2 }) // [2, 4, 6] println(list.mapIndexed { i, x -> "$i:$x" }) // [0:1, 1:2, 2:3] println(listOf("1", "a", "3").mapNotNull { it.toIntOrNull() }) // [1, 3] println(list.flatMap { listOf(it, it * 10) }) // [1, 10, 2, 20, 3, 30] println(listOf(listOf(1, 2), listOf(3)).flatten()) // [1, 2, 3] println(list.zip(listOf("a", "b"))) // [(1, a), (2, b)] println(list.withIndex().map { "${it.index}${it.value}" }) // [01, 12, 23] } ``` `mapNotNull` - самая полезная из них: преобразование с отбрасыванием неудач в одном шаге, вместо `map` + `filterNotNull`. `flatMap` нужен, когда преобразование даёт коллекцию, а результат нужен плоским: ```kotlin data class Order5(val id: Int, val items: List) fun main() { val orders = listOf( Order5(1, listOf("хлеб", "молоко")), Order5(2, listOf("сыр")), ) println(orders.flatMap { it.items }) // [хлеб, молоко, сыр] println(orders.map { it.items }) // [[хлеб, молоко], [сыр]] } ``` ## Фильтрация ```kotlin fun main() { val list = listOf(1, 2, 3, 4, 5) println(list.filter { it % 2 == 0 }) // [2, 4] println(list.filterNot { it % 2 == 0 }) // [1, 3, 5] println(list.filterIndexed { i, _ -> i > 2 }) // [4, 5] println(listOf(1, null, 3).filterNotNull()) // [1, 3] println(listOf(1, "a", 2).filterIsInstance()) // [1, 2] println(list.take(2)) // [1, 2] println(list.takeWhile { it < 3 }) // [1, 2] println(list.dropWhile { it < 3 }) // [3, 4, 5] println(list.distinct()) // [1, 2, 3, 4, 5] println(listOf("aa", "ab", "ba").distinctBy { it.first() }) // [aa, ba] } ``` `partition` делит на две коллекции за один проход: ```kotlin fun main() { val (even, odd) = listOf(1, 2, 3, 4).partition { it % 2 == 0 } println("$even $odd") // [2, 4] [1, 3] } ``` ## Поиск и проверки ```kotlin fun main() { val list = listOf(1, 2, 3, 4) println(list.first { it > 2 }) // 3, бросает если нет println(list.firstOrNull { it > 10 }) // null println(list.find { it > 2 }) // 3 — то же, что firstOrNull println(list.last()) // 4 println(list.single { it == 2 }) // 2, бросает если не ровно один println(list.singleOrNull { it > 2 }) // null — их два println(list.any { it > 3 }) // true println(list.all { it > 0 }) // true println(list.none { it > 10 }) // true println(list.count { it % 2 == 0 }) // 2 println(3 in list) // true println(list.indexOfFirst { it > 2 }) // 2 } ``` Поведение на пустой коллекции - источник ошибок: ```kotlin fun main() { val empty = emptyList() println(empty.any { it > 0 }) // false println(empty.all { it > 0 }) // true — вакуумная истина! println(empty.none { it > 0 }) // true println(empty.sum()) // 0 println(empty.firstOrNull()) // null println(empty.maxOrNull()) // null } ``` `all` на пустой коллекции возвращает `true` - это математически корректно, но в проверках вида «все элементы валидны» часто неожиданно. ## Агрегация ```kotlin fun main() { val list = listOf(1, 2, 3, 4) println(list.sum()) // 10 println(list.average()) // 2.5 println(list.maxOrNull()) // 4 println(list.minOrNull()) // 1 println(list.sumOf { it * 10 }) // 100 println(list.maxByOrNull { -it }) // 1 println(list.maxOfOrNull { it * 2 }) // 8 println(list.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3, 4] println(list.joinToString(limit = 2, truncated = "…")) // 1, 2, … } ``` `maxBy` против `maxOf` - частая путаница: ```kotlin data class User8(val name: String, val age: Int) fun main() { val users = listOf(User8("Иван", 30), User8("Пётр", 25)) println(users.maxByOrNull { it.age }) // User8(name=Иван, age=30) — элемент println(users.maxOfOrNull { it.age }) // 30 — значение } ``` ## fold и reduce ```kotlin fun main() { val list = listOf(1, 2, 3, 4) println(list.fold(0) { acc, x -> acc + x }) // 10 println(list.fold(1) { acc, x -> acc * x }) // 24 println(list.reduce { acc, x -> acc + x }) // 10 println(list.runningFold(0) { acc, x -> acc + x }) // [0, 1, 3, 6, 10] println(list.foldRight(0) { x, acc -> acc * 10 + x }) // 4321 — справа налево println(emptyList().fold(0) { a, b -> a + b }) // 0 try { emptyList().reduce { a, b -> a + b } } catch (e: UnsupportedOperationException) { println("reduce падает на пустой коллекции") } } ``` | | `fold` | `reduce` | |---|---|---| | начальное значение | задаёте вы | первый элемент | | тип результата | любой | тип элемента | | пустая коллекция | вернёт начальное | исключение | `fold` умеет менять тип, и это его главное применение: ```kotlin fun main() { val words = listOf("раз", "два", "три") val lengths: Map = words.fold(mutableMapOf()) { acc, w -> acc[w] = w.length acc } println(lengths) // {раз=3, два=3, три=3} println(words.fold(0) { acc, w -> acc + w.length }) // 9 — List -> Int } ``` ## Группировка ```kotlin fun main() { val words = listOf("яблоко", "арбуз", "банан", "боб") println(words.groupBy { it.first() }) // {я=[яблоко], а=[арбуз], б=[банан, боб]} println(words.groupBy(keySelector = { it.first() }, valueTransform = { it.length })) // {я=[6], а=[5], б=[5, 3]} println(words.groupingBy { it.first() }.eachCount()) // {я=1, а=1, б=2} println(words.groupingBy { it.first() }.fold(0) { acc, w -> acc + w.length }) // {я=6, а=5, б=8} } ``` `groupingBy` отличается от `groupBy` тем, что не создаёт промежуточные списки - он сразу агрегирует. Для подсчёта и суммирования по группам это и быстрее, и короче. ## associate ```kotlin fun main() { val users = listOf("Иван", "Пётр", "Анна") println(users.associateWith { it.length }) // {Иван=4, Пётр=4, Анна=4} println(users.associateBy { it.first() }) // {И=Иван, П=Пётр, А=Анна} println(users.associate { it.first() to it.length }) // {И=4, П=4, А=4} } ``` | Функция | Ключ | Значение | |---|---|---| | `associateWith` | элемент | результат лямбды | | `associateBy` | результат лямбды | элемент | | `associate` | из `Pair` | из `Pair` | `associateBy` против `groupBy` - важная разница: ```kotlin fun main() { val words = listOf("банан", "боб") println(words.associateBy { it.first() }) // {б=боб} — потеря! println(words.groupBy { it.first() }) // {б=[банан, боб]} } ``` `associateBy` при дубликате ключа **молча перезаписывает**. Если ключ не уникален, нужен `groupBy`. ## Сортировка ```kotlin fun main() { val list = listOf(3, 1, 2) println(list.sorted()) // [1, 2, 3] println(list.sortedDescending()) // [3, 2, 1] println(list.reversed()) // [2, 1, 3] — не сортировка! println(list.shuffled().size) // 3 val users = listOf("Пётр" to 25, "Иван" to 30, "Анна" to 25) println(users.sortedBy { it.second }) // [(Пётр, 25), (Анна, 25), (Иван, 30)] println(users.sortedWith(compareBy({ it.second }, { it.first }))) // [(Анна, 25), (Пётр, 25), (Иван, 30)] — по возрасту, потом по имени println(users.sortedWith(compareByDescending> { it.second } .thenBy { it.first })) // [(Иван, 30), (Анна, 25), (Пётр, 25)] } ``` Сортировка Kotlin **стабильна**: элементы с равными ключами сохраняют исходный порядок. На `MutableList` есть сортировка на месте - без `ed` в имени: ```kotlin fun main() { val list = mutableListOf(3, 1, 2) list.sort() // на месте println(list) // [1, 2, 3] list.sortByDescending { it } println(list) // [3, 2, 1] val copy = list.sorted() // новая коллекция println("$list / $copy") // [3, 2, 1] / [1, 2, 3] } ``` Соглашение в stdlib: `sort()` меняет на месте, `sorted()` возвращает копию. То же с `reverse()`/`reversed()`, `shuffle()`/`shuffled()`. ## Разбиение и объединение ```kotlin fun main() { val list = listOf(1, 2, 3, 4, 5) println(list.chunked(2)) // [[1, 2], [3, 4], [5]] println(list.chunked(2) { it.sum() }) // [3, 7, 5] println(list.windowed(3)) // [[1,2,3], [2,3,4], [3,4,5]] println(list.windowed(2, step = 2, partialWindows = true)) // [[1,2], [3,4], [5]] println(list.zipWithNext()) // [(1,2), (2,3), (3,4), (4,5)] println(list.zipWithNext { a, b -> b - a }) // [1, 1, 1, 1] } ``` `windowed` - скользящее окно: удобно для расчёта разниц, средних и сглаживания. ## Операции на Map ```kotlin fun main() { val map = mapOf("a" to 1, "b" to 2, "c" to 3) println(map.filter { (_, v) -> v > 1 }) // {b=2, c=3} println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30} println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3} println(map.filterKeys { it != "a" }) // {b=2, c=3} println(map.maxByOrNull { it.value }?.key) // c println(map.entries.sumOf { it.value }) // 6 println(map.toList().map { (k, v) -> "$k$v" }) // [a1, b2, c3] } ``` Деструктуризация в лямбде (`{ (k, v) -> }`) работает для `Map.Entry` и для `Pair`. ## Практический пример ```kotlin data class Sale(val region: String, val product: String, val amount: Long) fun main() { val sales = listOf( Sale("Север", "хлеб", 100), Sale("Север", "молоко", 200), Sale("Юг", "хлеб", 150), Sale("Юг", "сыр", 300), ) val byRegion = sales.groupBy { it.region }.mapValues { (_, v) -> v.sumOf { it.amount } } println(byRegion) // {Север=300, Юг=450} val top = sales.maxByOrNull { it.amount } println("${top?.product}: ${top?.amount}") // сыр: 300 val products = sales.map { it.product }.distinct().sorted() println(products) // [молоко, сыр, хлеб] val report = byRegion.entries .sortedByDescending { it.value } .joinToString("\n") { "${it.key}: ${it.value}" } println(report) // Юг: 450\nСевер: 300 } ``` > [!info] Отличия от Java > | Java Stream | Kotlin | > |---|---| > | `stream().map(f).collect(toList())` | `map(f)` | > | `stream().filter(p).collect(toList())` | `filter(p)` | > | `collect(groupingBy(f))` | `groupBy(f)` | > | `collect(toMap(k, v))` | `associate { k to v }` | > | `reduce(0, Integer::sum)` | `fold(0) { a, b -> a + b }` | > | `mapToInt(f).sum()` | `sumOf(f)` | > | `flatMap(x -> x.stream())` | `flatMap { it }` | > | `collect(partitioningBy(p))` | `partition(p)` | > | `anyMatch(p)` | `any(p)` | > | нет аналога | `mapNotNull`, `windowed`, `zipWithNext`, `groupingBy` | > > **Главное - не нужен `.stream()` и `.collect()`.** В Java любая операция требует входа в стрим и выхода из него; это ~25 символов обвязки на каждую цепочку. В Kotlin операции объявлены прямо на `Iterable`, поэтому короткие цепочки читаются как одно выражение: > ```kotlin > fun main() { > println(listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 10 }) // 50 > } > ``` > ```java > List.of(1, 2, 3).stream().filter(x -> x > 1).mapToInt(x -> x * 10).sum(); > ``` > > **Цена - энергичность.** Java-стрим ленив: промежуточные операции не создают коллекций. Kotlin-операции на `Iterable` создают новый список на каждом шаге. Для цепочки из 3 операций по списку из 1000 элементов это 3 списка. Ленивый аналог - [Sequence](Sequence.md), и он почти дословно соответствует `Stream`. > > **`mapNotNull` и `groupingBy` аналогов в Java не имеют.** В Java фильтрация с преобразованием - это `map` + `filter(Objects::nonNull)` или `flatMap` с `Optional.stream()`. Подсчёт по группам - `groupingBy(f, counting())`, то есть вложенные коллекторы, которые плохо читаются. > > **`Optional` в цепочках не нужен.** Java-стримы возвращают `Optional` из `findFirst` и `max`; в Kotlin это nullable-тип, с которым работают обычные `?.` и `?:`: > ```kotlin > fun main() { > println(listOf(1, 2, 3).maxOrNull() ?: 0) // 3 > } > ``` > > Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) > [!warning] Подводные камни > **Каждая операция создаёт новую коллекцию:** > ```kotlin > fun main() { > val big = List(100_000) { it } > val result = big.map { it * 2 }.filter { it % 3 == 0 }.take(5) > println(result) // [0, 6, 12, 18, 24] > } > ``` > `map` обработал все 100 000 элементов и создал список, `filter` - ещё один, и только потом `take` взял пять. С `asSequence()` работы было бы на порядки меньше. > > **`all` на пустой коллекции - `true`:** > ```kotlin > fun main() { > val errors = emptyList() > println(errors.all { it.isNotBlank() }) // true > println(errors.isNotEmpty() && errors.all { it.isNotBlank() }) // false > } > ``` > Проверка «все данные валидны» на пустом входе пройдёт. Если пустота недопустима, проверяйте её отдельно. > > **`associateBy` теряет дубликаты молча** - при повторе ключа остаётся последний элемент. Для неуникальных ключей нужен `groupBy`. > > **`reduce` падает на пустой коллекции**, `fold` - нет. В коде, где коллекция может быть пустой, `reduce` - потенциальное исключение. > > **`sortedBy` с nullable-ключом ставит `null` первыми:** > ```kotlin > fun main() { > val list = listOf("b" to 2, "a" to null, "c" to 1) > println(list.sortedBy { it.second }) // [(a, null), (c, 1), (b, 2)] > println(list.sortedWith(compareBy(nullsLast()) { it.second })) // [(c, 1), (b, 2), (a, null)] > } > ``` > > **`reversed()` - не сортировка**, а переворот. `listOf(3, 1, 2).reversed()` даёт `[2, 1, 3]`. > > **`sort()` и `sorted()` легко перепутать.** `sort()` молча меняет исходную коллекцию - если она пришла извне, это неожиданная мутация: > ```kotlin > fun process(items: MutableList): List { > items.sort() // испортили аргумент вызывающего > return items > } > > fun main() { > val original = mutableListOf(3, 1, 2) > process(original) > println(original) // [1, 2, 3] — вызывающий этого не просил > } > ``` > Принимайте `List` вместо `MutableList` - тогда `sort()` просто не скомпилируется. > > **`first()` и `single()` бросают, а не возвращают `null`.** Версии с `OrNull` безопасны; `single` вдобавок падает, если элементов больше одного. > > **`flatMap { it }` на `List>` работает, но `flatten()` яснее.** > > **Цепочка из `filter` после `map` делает лишнюю работу.** `filter` перед `map` обрабатывает меньше элементов: > ```kotlin > fun main() { > val list = List(10) { it } > println(list.map { it * 2 }.filter { it > 10 }) // [12, 14, 16, 18], 10 преобразований > println(list.filter { it > 5 }.map { it * 2 }) // [12, 14, 16, 18], 4 преобразования > } > ``` > Здесь результаты совпадают, а работы во втором случае вдвое с лишним меньше. Но совпадают они не всегда: перестановка `filter` и `map` меняет смысл, если предикат проверяет уже преобразованное значение. Сначала убедитесь в эквивалентности, потом выбирайте по стоимости. > > **`joinToString` по умолчанию без ограничения длины.** Для списка на миллион элементов он построит огромную строку; в логах используйте `limit`. Далее: [Sequence](Sequence.md)