Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user