Коллекции Kotlin становятся особенно понятными, когда в списке лежат не абстрактные числа, а реальные объекты. В этом уроке мы возьмем список заказов, отфильтруем оплаченные, посчитаем выручку, сгруппируем по городам и сделаем маленькую домашку
Главная мысль: filter, map, groupBy и sumOf обычно возвращают новый результат, а не меняют исходную коллекцию. Это важное отличие от операций, которые меняют MutableList
- Что получится в конце
- List и MutableList
- filter: оставить нужные элементы
- map: превратить элементы
- sumOf: посчитать сумму
- groupBy: сгруппировать по ключу
- Домашка: средний чек
- Мини-практика: топ городов
- any, all и count
- associateBy и быстрый поиск
- Домашка: отчет по заказам
- Когда цепочки становятся слишком длинными
- Частые ошибки и порядок проверки
- Мини-чек для цепочек коллекций
- Домашка со звездочкой: найти лучший город
- Что может быть еще интересно по этой теме
- Что почитать дальше по Kotlin
Что получится в конце
Код:
data class Order(
val id: Int,
val city: String,
val total: Int,
val paid: Boolean
)
fun main() {
val orders = listOf(
Order(1, "Kazan", 1200, true),
Order(2, "Moscow", 3400, false),
Order(3, "Kazan", 800, true),
Order(4, "Samara", 1600, true)
)
val paidOrders = orders.filter { it.paid }
val revenue = paidOrders.sumOf { it.total }
val byCity = paidOrders.groupBy { it.city }
println("Paid orders: $paidOrders")
println("Revenue: $revenue")
println("By city: $byCity")
}
Ожидаемый смысл вывода:
Revenue: 3600
Заказы из Казани попадут в одну группу, неоплаченный заказ из Москвы не попадет в выручку
List и MutableList
listOf создает read-only список:
val orders = listOf(...)
Это не значит, что внутри Вселенной объект физически невозможно изменить в любой ситуации. Но через интерфейс List вы не можете делать add или remove
Если нужен изменяемый список:
val tasks = mutableListOf("write", "check")
tasks.add("publish")
Практическое правило: начинайте с listOf. Переходите к mutableListOf, только когда задача действительно требует изменения списка
filter: оставить нужные элементы
Фильтрация:
val paidOrders = orders.filter { it.paid }
it — текущий элемент списка. Условие it.paid оставляет только оплаченные заказы
Важно: filter не меняет orders. Он возвращает новый список. Если написать:
orders.filter { it.paid }
и не сохранить результат, ничего полезного дальше не произойдет. Это частая ошибка новичков
map: превратить элементы
Если нужно получить только суммы:
val totals = orders.map { it.total }
Теперь totals — список Int
Можно собрать строки:
val labels = orders.map { "Order #${it.id}: ${it.total}" }
map не фильтрует сам по себе. Он преобразует каждый элемент в новый вид. Если нужны только оплаченные заказы, сначала filter, потом map
sumOf: посчитать сумму
Выручка:
val revenue = paidOrders.sumOf { it.total }
Это короче и понятнее, чем ручной цикл с переменной sum
Ручной цикл иногда полезен для обучения:
var revenue = 0
for (order in paidOrders) {
revenue += order.total
}
Но в Kotlin для типичных операций над коллекциями лучше знать стандартные функции. Они читаются ближе к смыслу задачи
groupBy: сгруппировать по ключу
Группировка:
val byCity = paidOrders.groupBy { it.city }
Результат имеет смысл:
Map<String, List<Order>>
То есть ключ — город, значение — список заказов из этого города
Если нужна выручка по городам, можно продолжить:
val revenueByCity = paidOrders
.groupBy { it.city }
.mapValues { (_, cityOrders) ->
cityOrders.sumOf { it.total }
}
Здесь важно не испугаться цепочки. Читайте ее слева направо: взять оплаченные, сгруппировать по городу, для каждой группы посчитать сумму
Домашка: средний чек
Добавьте функцию:
fun averagePaidOrder(orders: List<Order>): Double
Правила:
- учитывать только
paid == true - если оплаченных заказов нет, вернуть
0.0 - результат вывести через
println
Подсказка: используйте filter, sumOf, size и проверку на пустой список
Мини-практика: топ городов
Попробуйте вывести города и выручку:
revenueByCity.forEach { (city, revenue) ->
println("$city: $revenue")
}
Если хотите отсортировать:
val sorted = revenueByCity.toList().sortedByDescending { it.second }
toList() превращает map в список пар, а sortedByDescending сортирует по второму значению пары. Для первого урока это уже немного продвинутый шаг, но он хорошо показывает, как операции коллекций собираются в pipeline
any, all и count
Кроме filter и map, в повседневном коде часто нужны проверки:
val hasUnpaid = orders.any { !it.paid }
val allPaid = orders.all { it.paid }
val paidCount = orders.count { it.paid }
any отвечает: есть ли хотя бы один элемент. all проверяет все элементы. count считает элементы по условию
Эти функции часто читаются лучше, чем ручной цикл с флагом:
var hasUnpaid = false
for (order in orders) {
if (!order.paid) {
hasUnpaid = true
}
}
Ручной цикл полезен для понимания, но стандартные операции делают намерение видимым прямо в названии
associateBy и быстрый поиск
Если нужно быстро искать заказ по id:
val byId = orders.associateBy { it.id }
val order = byId[3]
associateBy создает map, где ключом становится результат lambda. В нашем случае это id, а значением — сам Order
Если id повторяются, последнее значение по ключу перезапишет предыдущее. Поэтому перед associateBy важно понимать, действительно ли ключ уникален
Домашка: отчет по заказам
Напишите функцию:
fun report(orders: List<Order>): String
Она должна вернуть строку:
Paid: 3, unpaid: 1, revenue: 3600
Используйте count, filter и sumOf. Дополнительная задача: добавить выручку по городам через groupBy и mapValues
Когда цепочки становятся слишком длинными
Kotlin позволяет писать цепочки:
orders
.filter { it.paid }
.groupBy { it.city }
.mapValues { (_, items) -> items.sumOf { it.total } }
Это нормально, пока каждый шаг легко читается. Если цепочка стала длиннее экрана, разбейте ее на переменные:
val paidOrders = orders.filter { it.paid }
val ordersByCity = paidOrders.groupBy { it.city }
val revenueByCity = ordersByCity.mapValues { (_, items) -> items.sumOf { it.total } }
Такой код чуть длиннее, зато проще объяснять и отлаживать
Частые ошибки и порядок проверки
Думаете, что filter меняет список filter возвращает новый список. Сохраните результат в переменную или передайте дальше по цепочке
Используете MutableList без необходимости Если список не меняется после создания, List проще и безопаснее
Пугает it it — имя единственного параметра lambda по умолчанию. Если так не читается, напишите имя явно: orders.filter { order -> order.paid }
Смешиваете map и groupBy map преобразует каждый элемент. groupBy раскладывает элементы по ключам
Мини-чек для цепочек коллекций
Перед тем как оставить цепочку операций в финальном коде, проговорите ее обычным языком. Например:
orders
.filter { it.paid }
.groupBy { it.city }
.mapValues { (_, items) -> items.sumOf { it.total } }
Читается так: взять заказы, оставить оплаченные, сгруппировать по городу, посчитать сумму в каждой группе
Если вы не можете так же просто объяснить цепочку, разбейте ее на переменные. Kotlin не требует писать все в одну строку. Хороший код можно объяснить без пальца на экране
Домашка со звездочкой: найти лучший город
После revenueByCity попробуйте найти город с максимальной выручкой:
val bestCity = revenueByCity.maxByOrNull { it.value }
println(bestCity)
maxByOrNull возвращает nullable-результат, потому что map может быть пустой. Это еще одна точка, где коллекции встречаются с null safety
Что может быть еще интересно по этой теме
Коллекции Kotlin отличаются от Java Collections? Kotlin работает поверх JVM-экосистемы, но дает более удобные функции, read-only интерфейсы и лаконичный синтаксис lambda
Что быстрее: цепочка операций или цикл? Для первых уроков важнее читаемость. В горячем коде нужно измерять. Не оптимизируйте учебный пример до того, как он стал понятным
Когда использовать sequence? Когда цепочки большие или данные обрабатываются лениво. Для маленьких списков List и обычные операции проще
Что дальше после коллекций? Coroutines. Там коллекции и data class часто используются вместе с загрузкой данных, задержками, сетевыми запросами и UI
Что почитать дальше по Kotlin
- Функции и data class в Kotlin
- Coroutines в Kotlin: первый async-пример
- Kotlin Android: первый экран без перегруза
- Kotlin и Java: в чем разница для новичка



