Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,424 @@
---
title: Операции
description: map, filter, groupBy, fold, zip и остальной арсенал операций над коллекциями
tags:
- kotlin
- collections
---
Операции над коллекциями - функции-расширения на `Iterable<T>`. Их около 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<String>)
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<Any>(1, "a", 2).filterIsInstance<Int>()) // [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<Int>()
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<Int>().fold(0) { a, b -> a + b }) // 0
try {
emptyList<Int>().reduce { a, b -> a + b }
} catch (e: UnsupportedOperationException) {
println("reduce падает на пустой коллекции")
}
}
```
| | `fold` | `reduce` |
|---|---|---|
| начальное значение | задаёте вы | первый элемент |
| тип результата | любой | тип элемента |
| пустая коллекция | вернёт начальное | исключение |
`fold` умеет менять тип, и это его главное применение:
```kotlin
fun main() {
val words = listOf("раз", "два", "три")
val lengths: Map<String, Int> = 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<String> -> 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<Pair<String, Int>> { 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<String>()
> 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<Int>): List<Int> {
> 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<List<T>>` работает, но `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)