Files
obsidian/Kotlin/Коллекции/Операции.md
T

425 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)