Коллекции Kotlin: list, map, filter и groupBy

Коллекции Kotlin становятся особенно понятными, когда в списке лежат не абстрактные числа, а реальные объекты. В этом уроке мы возьмем список заказов, отфильтруем оплаченные, посчитаем выручку, сгруппируем по городам и сделаем маленькую домашку

Главная мысль: filter, map, groupBy и sumOf обычно возвращают новый результат, а не меняют исходную коллекцию. Это важное отличие от операций, которые меняют MutableList

Что получится в конце

Код:

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x