JavaRush /Курсы /Kotlin SELF /groupingBy().eachCount() — считаем частоты без списков гр...

groupingBy().eachCount() — считаем частоты без списков групп

Kotlin SELF
24 уровень , 1 лекция
Открыта

1. Частоты: что именно мы хотим получить

Когда вы впервые слышите «частоты», можно представить себе скучную статистику, но на практике это одна из самых полезных мини-операций над данными. Частоты — это ответ на вопрос «сколько раз встретилось…». Сколько расходов в каждой категории, сколько раз пользователь вводил ту или иную команду, сколько слов каждого типа есть в тексте — всё это частоты.

В терминах Kotlin-типов мы обычно хотим получить вот такую структуру:

  • ключ — это «что считаем» (категория, слово, первая буква, число и т.д.)
  • значение — это «сколько раз встретилось»

То есть результатом будет карта частот:

Map<K, Int>

Пример «по-человечески»:

  • "food" -> 12 (в категории food 12 записей)
  • "taxi" -> 3
  • "kotlin" -> 2

В этот момент мозг часто предлагает «давайте сделаем MutableMap и руками увеличивать счётчик». Это нормальный план, но Kotlin-стандартная библиотека умеет делать это в одну строчку — и именно здесь появляется связка groupingBy { ... }.eachCount().

2. Почему groupBy не всегда подходит для подсчёта

Когда вы уже знаете groupBy, возникает очень естественная мысль: «Окей, сгруппирую, а потом возьму size у каждой группы». Работает? Работает. Удобно? Иногда да. Но есть нюанс: groupBy возвращает Map<K, List<T>>, то есть по каждому ключу он хранит целый список элементов.

А если вам нужен только ответ «сколько» — то хранение списков становится лишним:

  • списки занимают память;
  • списки нужно создавать и наполнять;
  • потом вы всё равно превращаете список в число (size) и выбрасываете подробности.

Это немного похоже на ситуацию: вам нужно знать «сколько людей пришло на вечеринку», а вы вместо этого заводите отдельную комнату для каждого имени, загоняете туда людей, а потом считаете, сколько в комнате человек… и тут же всех выгоняете. Мощно, но слегка театрально.

Для «просто посчитать» Kotlin предлагает другой путь: сначала сказать «я умею вычислять ключ для каждого элемента», а потом попросить «посчитай, сколько элементов у каждого ключа». Именно это и делает groupingBy { ... }.eachCount().

3. Что такое groupingBy { ... } и почему это ещё не Map

После groupBy у многих возникает ожидание: «любая группировка возвращает Map». Но groupingBy работает иначе. Он возвращает не карту, а специальный объект типа Grouping, который является «заготовкой для группировки»: в нём хранится логика вычисления ключа, а конкретный результат появляется позже, когда вы применяете агрегирующую операцию.

Давайте посмотрим на это ощущениями, а не только типами. Мы можем разбить мысль на два шага:

  1. «Вот правило, как из элемента получить ключ»
  2. «Примените к этому правилу операцию: например, посчитайте элементы»

Код выглядит так:

fun main() {
    val words = listOf("one", "two", "one")

    val grouping = words.groupingBy { it } // пока не Map!
    val freq = grouping.eachCount()        // вот теперь Map<String, Int>

    println(freq) // {one=2, two=1}
}

Здесь важно почувствовать: groupingBy { ... } — это как «оформить заявку», а eachCount() — это как «получить готовый отчёт».

4. eachCount(): получаем карту частот Map<K, Int>

Когда вы вызываете eachCount(), Kotlin делает ровно то, что нам нужно: возвращает карту, где для каждого ключа записано количество элементов.

Самый базовый пример: частоты слов

fun main() {
    val words = listOf("one", "two", "one", "three", "two")

    val freq: Map<String, Int> = words
        .groupingBy { it }
        .eachCount()

    println(freq) // {one=2, two=2, three=1}
}

Заметьте приятный момент: нам не нужно ни MutableMap, ни put, ни «если заметили ключ — увеличь, иначе — поставь 1». Kotlin всё это делает сам.

Пример с вычисляемым ключом: группируем по первой букве

Этот пример хорош тем, что ключ (Char) отличается от исходного элемента (String).

fun main() {
    val words = listOf("one", "two", "three", "four", "five")

    val freqByFirstChar = words.groupingBy { it.first() }.eachCount()

    for ((ch, count) in freqByFirstChar) {
        println("$ch -> $count")
        // o -> 1
        // t -> 2
        // f -> 2
    }
}

Вы здесь одновременно закрепляете сразу три навыка: лямбду в groupingBy, работу с Char и обход Map через деконструкцию (key, value).

5. Нормализация ключа перед подсчётом

В реальных данных чаще всего проблема не в подсчёте, а в том, что «одно и то же» выглядит по-разному. Пробелы, регистр, случайные лишние символы — всё это создаёт “ложные разные ключи”.

И тут нужно запомнить простое правило: нормализуйте ключ прямо внутри groupingBy { ... }, потому что именно там решается, что считается «одинаковым».

fun main() {
    val drinks = listOf("Tea", " tea ", "TEA", "Coffee")

    val freq = drinks
        .groupingBy { it.trim().lowercase() }
        .eachCount()

    println(freq) // {tea=3, coffee=1}
}

Это один из тех моментов, где Kotlin буквально подталкивает к правильному дизайну: «ключ группировки» — это отдельное понятие. Не “как в исходном вводе”, а “как в вашем внутреннем мире данных”.

Если в вашем проекте категории заметно «плывут» (например, пользователь ввёл " Taxi " вместо "taxi"), нормализация не просто улучшает статистику — она спасает весь смысл отчёта.

6. Практика: считаем количество расходов по категориям

С этого дня у нас в курсе уже есть практический консольный проект (мы стартовали его на коллекциях): условный учёт расходов. Пока без ООП, поэтому хранить записи будем максимально просто: как Triple(category, amount, note).

Сделаем небольшую заготовку данных и функцию, которая считает «сколько записей в каждой категории». Обратите внимание: мы сегодня считаем количество записей, а не сумму денег. Сумма — тоже важная штука, но это другая операция.

Данные и подсчёт количества по категории

fun main() {
    val expenses: List<Triple<String, Int, String>> = listOf(
        Triple("food", 120, "groceries"),
        Triple("Food ", 80, "snack"),
        Triple("taxi", 250, "airport"),
        Triple("TAXI", 90, "home")
    )

    val countsByCategory = expenses
        .groupingBy { (category, _, _) -> category.trim().lowercase() }
        .eachCount()

    println(countsByCategory) // {food=2, taxi=2}
}

Здесь полезно остановиться и прочитать лямбду глазами: (category, _, _) -> .... Это обычная деконструкция Triple, которую вы уже умеете с дня про Pair/Triple. Мы явно говорим: «ключ — это нормализованная категория».

Выносим в функцию (чтобы main не распухал)

fun countExpensesByCategory(expenses: List<Triple<String, Int, String>>): Map<String, Int> {
    return expenses
        .groupingBy { (category, _, _) -> category.trim().lowercase() }
        .eachCount()
}

fun main() {
    val expenses = listOf(
        Triple("food", 120, "groceries"),
        Triple("Food ", 80, "snack"),
        Triple("taxi", 250, "airport")
    )

    val counts = countExpensesByCategory(expenses)
    println(counts) // {food=2, taxi=1}
}

С практической точки зрения это очень важный шаг: countExpensesByCategory(...) становится маленьким кирпичиком отчётов. Он возвращает чистый результат (карту), а не печатает всё сам — значит, позже мы сможем красиво форматировать вывод как угодно.

7. Ключ может быть любым: строки, числа, булевы значения

Иногда кажется, что частоты — это «только для слов». Но на самом деле ключом может быть любой тип: Int, Char, Boolean, даже сложные штуки (но мы пока не усложняем, мы ещё бережём психику).

Частоты чисел

fun main() {
    val numbers = listOf(10, 10, 20, 30, 30, 30)

    val freq = numbers.groupingBy { it }.eachCount()

    for ((n, count) in freq) {
        println("$n -> $count")
        // 10 -> 2
        // 20 -> 1
        // 30 -> 3
    }
}

Частоты «чётное / нечётное»

Ключом будет Boolean: true для чётных, false для нечётных. Это звучит странно ровно до тех пор, пока вы не увидите, насколько это удобно.

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)

    val freq = numbers
        .groupingBy { it % 2 == 0 }
        .eachCount()

    println(freq) // {false=3, true=3}
}

Если вы потом захотите вывести это человеческими словами (“чётных столько-то”), это будет уже вопрос форматирования, а не подсчёта — то есть правильное разделение ответственности.

Ручной счётчик vs eachCount()

Иногда полезно один раз увидеть «как было бы вручную», чтобы сильнее оценить, что делает стандартная библиотека. Представьте, что вам дали задачу «посчитать частоты категорий», и вы пишете через MutableMap.

fun main() {
    val categories = listOf("food", "food", "taxi")

    val counts = mutableMapOf<String, Int>()
    for (c in categories) {
        val old = counts[c] ?: 0
        counts[c] = old + 1
    }

    println(counts) // {food=2, taxi=1}
}

Это нормальный код, и он даже полезен, чтобы понимать суть алгоритма. Но в реальном проекте хочется короче и выразительнее:

fun main() {
    val categories = listOf("food", "food", "taxi")

    val counts = categories.groupingBy { it }.eachCount()
    println(counts) // {food=2, taxi=1}
}

Важная мысль: eachCount() не «магия из космоса». Это просто аккуратная, стандартная реализация паттерна «счётчик по ключу», которая делает код менее шумным.

8. Типичные ошибки при работе с groupingBy().eachCount()

Ошибка №1: ожидать, что groupingBy { ... } сразу вернёт Map.
После groupBy рука сама пишет val m = list.groupingBy { ... } и хочется тут же делать m["key"]. Но groupingBy возвращает промежуточный объект (по смыслу — «план группировки»), и карту вы получаете только после операции вроде eachCount(). Если вы видите в голове вопрос «почему не получается обращаться как к Map», значит вы забыли второй шаг.

Ошибка №2: считать частоты по “грязному ключу” и удивляться странным результатам.
Если категория "Taxi", " taxi " и "TAXI" считаются разными, проблема не в eachCount(): он честно считает то, что вы сказали считать. Решение — нормализовать ключ в groupingBy: чаще всего хватает trim().lowercase(). Это особенно критично в отчётах: неправильный ключ = неправильная статистика.

Ошибка №3: путать “количество записей” и “сумму значений”.
eachCount() считает, сколько элементов попало в группу. Если у вас расходы ("food", 100) и ("food", 200), результат по ключу "food" будет 2, а не 300. Это не баг и не подвох — это разные задачи. Сегодня мы решаем задачу “сколько”, а не “на какую сумму”.

Ошибка №4: печатать карту и ждать красивого порядка.
Когда вы делаете println(freq), Kotlin печатает карту «как получится» (порядок зависит от внутренней реализации). Это нормально для отладки, но для отчёта почти всегда нужен отдельный шаг: отсортировать, отформатировать, выровнять. Если у вас появляется ощущение «всё правильно, но выглядит некрасиво» — вы уткнулись не в проблему подсчёта, а в задачу представления результата, и это абсолютно ожидаемо.

Ошибка №5: делать слишком сложный ключ прямо внутри лямбды и терять читаемость.
Иногда хочется написать в groupingBy пол-экрана логики. Формально можно, но код быстро становится нечитабельным. Если ключ вычисляется сложно, лучше вынести вычисление ключа в отдельную функцию (например, normalizeCategory(category: String): String) и в groupingBy оставить одну понятную строку.

1
Задача
Kotlin SELF, 24 уровень, 1 лекция
Недоступна
Заказы напитков
Заказы напитков
1
Задача
Kotlin SELF, 24 уровень, 1 лекция
Недоступна
Стартовые буквы
Стартовые буквы
1
Задача
Kotlin SELF, 24 уровень, 1 лекция
Недоступна
Хэштеги на стенде
Хэштеги на стенде
1
Задача
Kotlin SELF, 24 уровень, 1 лекция
Недоступна
Журнал команд
Журнал команд
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
begem Уровень 60
1 апреля 2026
в задаче "Хэштеги на стенде" ответ принимается только если .filter и .groupingBy с trim()