Операции над коллекциями: 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-приложения.

Проверьте себя
1. Что делает операция map в Kotlin?
AУдаляет элементы, не прошедшие условие
BПреобразует каждый элемент коллекции по заданной лямбде и возвращает новую коллекцию
CСкладывает все элементы в одно значение
DСортирует коллекцию
2. Когда стоит использовать asSequence() в цепочке операций?
AВсегда, это быстрее в любом случае
BДля очень больших коллекций, чтобы избежать промежуточных списков (ленивая обработка)
CТолько для строк
DКогда нужна сортировка