Операции над коллекциями: map, filter, reduce
Kotlin предлагает богатый набор функций над коллекциями — map, filter, sortedBy, groupBy и другие, — которые заменяют ручные циклы декларативными цепочками преобразований.
Суть: вместо того чтобы писать циклы с накоплением, вы описываете, что хотите получить, — и код становится короче, читаемее и менее подвержен ошибкам.
Эти операции — функции высшего порядка: они принимают лямбду, описывающую преобразование. map превращает каждый элемент в новый. filter оставляет только подходящие. forEach выполняет действие для каждого. Все они возвращают новую коллекцию, не меняя исходную.
val nums = listOf(1, 2, 3, 4, 5, 6)
val squares = nums.map { it * it } // [1, 4, 9, 16, 25, 36]
val evens = nums.filter { it % 2 == 0 } // [2, 4, 6]
val sum = nums.reduce { acc, x -> acc + x } // 21
println(squares)
println(evens)
println(sum)Цепочки операций
Сила подхода — в комбинировании. Операции выстраиваются в конвейер: результат одной идёт на вход следующей. Это читается сверху вниз как описание задачи.
data class User(val name: String, val age: Int)
val users = listOf(
User("Аня", 19), User("Иван", 17),
User("Лена", 22), User("Пётр", 16)
)
val adultNames = users
.filter { it.age >= 18 } // взрослые
.sortedBy { it.age } // по возрасту
.map { it.name } // только имена
println(adultNames) // [Аня, Лена]Как работает под капотом
Каждая операция в цепочке создаёт промежуточную коллекцию. Для небольших списков это незаметно, но на больших объёмах память расходуется лишний раз. Для таких случаев есть asSequence(): он превращает обработку в ленивую — элементы проходят через всю цепочку по одному, без промежуточных списков. Для повседневных коллекций из десятков элементов обычные операции абсолютно нормальны.
Конвейер обработки данных
[1,2,3,4,5,6]
| filter { чётные }
v
[2,4,6]
| map { it * 10 }
v
[20,40,60]
| sum()
v
120Полезные операции
Кроме базовых, часто нужны: sortedBy — сортировка по ключу; groupBy — группировка в Map; any/all/none — проверки; first/firstOrNull — поиск; sumOf/count — агрегаты. mapNotNull преобразует и сразу выбрасывает null.
val byParity = nums.groupBy { if (it % 2 == 0) "чёт" else "нечёт" }
println(byParity) // {нечёт=[1,3,5], чёт=[2,4,6]}
println(nums.any { it > 5 }) // true
println(nums.all { it > 0 }) // true
println(nums.sumOf { it }) // 21Частые ошибки
Использовать forEach вместо map. Если нужен новый список, используйте map; forEach — только для побочных эффектов, он ничего не возвращает.
reduce на пустой коллекции. reduce на пустом списке бросит исключение; если список может быть пустым, берите fold с начальным значением.
Чрезмерно длинные цепочки. Десяток операций подряд тяжело читать; разбейте на промежуточные переменные с осмысленными именами.
Best practices
- Предпочитайте декларативные операции ручным циклам — меньше ошибок.
- Для очень больших коллекций используйте
asSequence(), чтобы избежать промежуточных списков. - Берите
firstOrNullвместоfirst, когда элемента может не быть. - Используйте
foldс начальным значением вместоreduceдля безопасности на пустых коллекциях.
Конвейер filter-map-sum легко повторить на Python — это та же логика, что в Kotlin. Запустите врезку.
# Аналог цепочки filter().map().sum() из Kotlin
nums = [1, 2, 3, 4, 5, 6]
result = sum(x * 10 for x in nums if x % 2 == 0)
print('сумма чётных, умноженных на 10:', result)
users = [('Аня', 19), ('Иван', 17), ('Лена', 22), ('Пётр', 16)]
adults = sorted([u for u in users if u[1] >= 18], key=lambda u: u[1])
print('взрослые:', [u[0] for u in adults])Попробуй сам ▶ — измените порог возраста и операцию преобразования. В Kotlin это были бы filter, sortedBy и map.
Закрепим главное
Закрепите сдвиг от «как вычислить» к «что получить». Императивный цикл описывает механику: заведи аккумулятор, пройди по элементам, на каждом шаге сделай то-то. Декларативная цепочка описывает результат: отфильтруй, преобразуй, сложи. Второй стиль читается как формулировка задачи и потому реже содержит ошибки в граничных случаях. Этот же стиль пронизывает весь современный Android: данные для экрана почти всегда готовятся именно цепочками операций над коллекциями.
Второй ориентир — выбор завершающей операции по намерению. Нужен новый список — map; отбор — filter; единственное значение — first/firstOrNull; агрегат — sumOf/count; группировка — groupBy. Точное имя операции делает код самодокументируемым: читающему не нужно разбирать тело цикла, чтобы понять, что происходит. А осознанный выбор между first и firstOrNull заранее закрывает вопрос пустой коллекции.
Итог: декларативные операции над коллекциями — одна из самых приятных черт Kotlin. Они превращают многострочные циклы в читаемые конвейеры. Эти же приёмы вы будете применять к спискам данных, которые показываете на экране Android-приложения.