JavaRush /Курсы /Kotlin SELF /find/any/all/count: поиск, проверки и подсчёты без ручных...

find/any/all/count: поиск, проверки и подсчёты без ручных циклов

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

1. Введение

Когда вы только начинаете программировать, самая естественная реакция на задачу «найти элемент» или «посчитать» — написать цикл и в нём делать проверки. Это нормально: мозг честно имитирует работу процессора. Но у такого подхода есть минусы: много шаблонного кода, легко ошибиться с break, легко забыть начальное значение счётчика, а иногда код получается таким, что его страшно трогать даже вам самим через два дня.

Операции find, any, all, count — это стандартные функции Kotlin для коллекций, которые выражают намерение напрямую: «найди», «проверь, что есть хотя бы один», «проверь, что все», «посчитай». Они читаются почти как русский текст, и код становится короче. А ещё это помогает отделять «мы считаем» от «мы печатаем», что делает программу предсказуемее.

Ниже будет много примеров. Мы начнём с очень маленьких списков, а потом применим всё к нашему консольному мини‑приложению учёта расходов, которое мы развиваем с дней про коллекции и команды.

2. find { ... }: найти первый подходящий элемент и подружиться с T?

find — это операция поиска. Вы даёте ей условие (лямбду‑предикат), и она возвращает первый элемент, который подходит. Если подходящего элемента нет, find возвращает null.

Это важный момент: тип результата будет nullable (T?), и компилятор Kotlin будет требовать, чтобы вы обработали ситуацию «ничего не нашли». Это не придирка, а забота: программа не должна падать от того, что элемент не нашёлся.

Начнём с простого примера на числах:

fun main() {
    val xs = listOf(1, 3, 4, 6)

    val firstEven: Int? = xs.find { it % 2 == 0 }
    println(firstEven) // 4
}

Здесь firstEven — это Int?. Даже если вы глазами видите, что чётное точно есть, Kotlin всё равно заставляет вас жить по правилам: «может быть null». Это дисциплина, которая позже сэкономит вам часы отладки.

Теперь пример, где элемента нет:

fun main() {
    val xs = listOf(1, 3, 5)

    val firstEven = xs.find { it % 2 == 0 }
    println(firstEven) // null
}

Обратите внимание: мы ничего не делали, чтобы «вернуть null». find сам так устроен: нет совпадений — получите null. Это и есть контракт.

find в CLI‑приложении: найти расход по id

Мы продолжаем условный CLI‑учёт расходов. По договорённости (пока без ООП) будем хранить один расход как Triple(id, category, amount), то есть Triple<Int, String, Int>. Это не идеальная модель, но сейчас нам важнее операции над коллекциями, а не красота доменной модели.

Сделаем функцию поиска расхода по id через find:

fun findExpenseById(
    expenses: List<Triple<Int, String, Int>>,
    id: Int
): Triple<Int, String, Int>? {
    return expenses.find { expense -> expense.first == id }
}

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

fun removeExpenseById(
    expenses: MutableList<Triple<Int, String, Int>>,
    id: Int
): Boolean {
    val found = expenses.find { it.first == id } ?: return false
    expenses.remove(found)
    return true
}

Здесь происходит аккуратная штука: если find вернул null, Elvis‑оператор ?: сразу делает return false. Это читается почти как разговор с программой: «если не нашли — сразу сообщи, что удалить не получилось».

3. any { ... } и all { ... }: проверки «есть хотя бы один» и «все подходят»

Иногда нам не нужен сам элемент. Нам нужен ответ «да/нет»: есть ли среди элементов такие, которые удовлетворяют условию? Или наоборот: все ли элементы удовлетворяют условию? Раньше вы бы заводили флаг var ok = false и меняли его в цикле. Kotlin говорит: «не мучайся» — есть any и all.

any { predicate } возвращает true, если хотя бы один элемент подходит под условие. all { predicate } возвращает true, если все элементы подходят под условие. Это две операции, которые очень любят жить в валидации и быстрых проверках «можно ли продолжать».

Вот пример:

fun main() {
    val words = listOf("a", "bb", "ccc")

    println(words.any { it.length == 2 }) // true
    println(words.all { it.isNotEmpty() }) // true
}

Важный крайний случай: пустая коллекция

Самый неожиданный момент (и классическая ловушка новичка): поведение any и all на пустой коллекции.

fun main() {
    val empty = emptyList<Int>()

    println(empty.any { it > 0 }) // false
    println(empty.all { it > 0 }) // true
}

Это не баг и не «странность Kotlin». Это логика из математики:

any на пустом наборе — false, потому что «нет ни одного элемента, который подтвердил бы условие».
all на пустом наборе — true, потому что «нет ни одного контрпримера». Никто не нарушил правило — формально правило выполнено.

Чтобы это было проще запомнить, держите маленькую таблицу:

Коллекция Проверка Результат Интуиция
[]
any { ... }
false
«Никого нет — значит, никто не подходит»
[]
all { ... }
true
«Никого нет — значит, никто не нарушил условие»

Когда это важно? Например, если вы проверяете «все суммы положительные». Если список пуст, all скажет true. Иногда это то, что вам нужно (пустой список не содержит ошибок). А иногда вы хотите отдельно проверить «список не пуст», но это уже часть бизнес‑логики, а не проблема all.

any/all в CLI‑приложении: быстрые проверки данных

Допустим, мы хотим понять: есть ли среди расходов очень крупные траты (скажем, больше 10_000). Это чистый any:

fun hasBigExpenses(
    expenses: List<Triple<Int, String, Int>>
): Boolean {
    return expenses.any { it.third > 10_000 }
}

И наоборот: хотим проверить, что все суммы корректные (например, строго больше нуля). Это all:

fun allExpensesPositive(
    expenses: List<Triple<Int, String, Int>>
): Boolean {
    return expenses.all { it.third > 0 }
}

Здесь важно держать в голове смысл: any/all — это не «похоже на if», это «ответ на вопрос». И чем точнее ваш вопрос, тем чище код.

4. count { ... }: подсчёт без внешнего счётчика

count — это операция, которая возвращает число элементов в коллекции. У неё есть два режима.

Если вызвать count() без условий — вы получите размер коллекции (то же, что size, но иногда удобно именно как «операция» в цепочке рассуждений). Если вызвать count { predicate }, вы получите количество элементов, удовлетворяющих условию. Kotlin‑документация относит count к агрегирующим операциям: возвращаем одно значение по содержимому коллекции.

Мини‑пример:

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

    val evenCount = xs.count { it % 2 == 0 }
    println(evenCount) // 2
}

Почему count { ... } лучше, чем filter { ... }.size

Да, можно написать так:

val evenCount = xs.filter { it % 2 == 0 }.size

Это будет работать. Но по смыслу вы делаете «фильтрацию» (получаете список), а потом берёте его размер. А вы ведь не хотели список — вы хотели число. count выражает намерение точнее и читается проще: «посчитай элементы, подходящие под условие». Плюс вы не создаёте промежуточный список (это уже вопрос эффективности, но даже без углубления приятно знать, что Kotlin за вас не делает лишней работы).

count в CLI‑приложении: простая статистика

Сделаем пару функций, которые считают, сколько у нас расходов в конкретной категории, и сколько всего:

fun totalExpenseCount(expenses: List<Triple<Int, String, Int>>): Int {
    return expenses.count()
}

fun countByCategory(
    expenses: List<Triple<Int, String, Int>>,
    category: String
): Int {
    return expenses.count { it.second == category }
}

Обратите внимание: count() возвращает Int, а count { ... } тоже возвращает Int. То есть это «финальный результат», который можно печатать, сохранять, сравнивать в if, и так далее.

5. Практический пример: команды remove и stats в CLI

Теперь сделаем кусок «практического» кода для нашего консольного приложения: добавим команду удаления по id через find, и команду stats, которая показывает несколько ответов через count/any/all. Важно: мы не строим идеальный проект, а тренируем операции коллекций, поэтому код будет максимально прямолинейным и небольшими кусками.

Печать одного расхода

Сделаем утилиту печати расхода:

fun printExpense(expense: Triple<Int, String, Int>) {
    val id = expense.first
    val category = expense.second
    val amount = expense.third

    println("#$id | $category | $amount")
}

Команда remove: «нашёл → удалил» без ручного обхода

fun handleRemove(
    expenses: MutableList<Triple<Int, String, Int>>,
    id: Int
) {
    val found = expenses.find { it.first == id }

    if (found == null) {
        println("Расход с id=$id не найден")
        return
    }

    expenses.remove(found)
    println("Удалено:")
    printExpense(found)
}

Тут сразу несколько полезных мыслей.

Во-первых, find даёт Triple<...>?, и мы честно проверяем null. Это как раз то, что в Kotlin называется «пишем код, который выживает в реальном мире».

Во-вторых, удаляем мы объектом found, а не индексом. Для MutableList это нормальный и понятный путь: нашли элемент — удалили этот элемент.

Команда stats: считаем и проверяем без циклов

fun printStats(expenses: List<Triple<Int, String, Int>>) {
    val total = expenses.count()
    val foodCount = expenses.count { it.second == "food" }
    val hasNegative = expenses.any { it.third < 0 }
    val allPositive = expenses.all { it.third > 0 }

    println("Всего расходов: $total")
    println("В категории food: $foodCount")
    println("Есть отрицательные суммы: $hasNegative")
    println("Все суммы положительные: $allPositive")
}

Заметьте, как этот код читается: он почти как текст отчёта. И самое приятное — здесь вообще не нужно заводить var counter, не нужно помнить про break, не нужно писать четыре цикла подряд.

Полезный приём: найти проблемный элемент через find

Если allPositive вдруг false, обычно хочется понять «а что именно сломано?». Для этого удобно сделать find на то же условие, только наоборот:

fun printFirstNegative(expenses: List<Triple<Int, String, Int>>) {
    val bad = expenses.find { it.third < 0 }

    if (bad != null) {
        print("Первый подозрительный расход: ")
        printExpense(bad)
    }
}

any отвечает на вопрос «существует ли проблема», а find помогает показать «пример проблемы». Это очень жизненная связка: валидация + диагностика.

6. Типичные ошибки при использовании find/any/all/count

Ошибка №1: забыть, что find возвращает T?, и обращаться к результату как к non-null.
Самая частая история выглядит так: вы написали val x = list.find { ... }, а потом сразу println(x.first) (или x.length, если это строка). Компилятор не даст вам это сделать — и правильно. Нужно либо проверять x != null, либо использовать safe-call x?.first, либо задавать поведение по умолчанию через Elvis ?:. Когда вы привыкаете к этому, код становится устойчивее, а «случайные падения» исчезают как жанр.

Ошибка №2: путать find и filter, ожидая «все подходящие», а не «первый подходящий».
find возвращает один элемент (первый), а не список. Если вам нужно собрать все подходящие элементы, это уже стиль filter (мы это обсуждали в предыдущей лекции про map/filter). Ошибка обычно проявляется тихо: вы думаете, что нашли «все большие расходы», а на самом деле нашли только первый и успокоились. Это один из тех багов, которые особенно любят жить в отчётах.

Ошибка №3: удивляться поведению all на пустой коллекции.
Если у вас expenses пустой, expenses.all { it.third > 0 } вернёт true. Это корректно по контракту, но иногда не соответствует смыслу «у нас есть хоть какие-то данные». Если вам принципиально важно отличать «всё хорошо» от «у нас нет данных», добавляйте отдельную проверку на пустоту там, где формулируется бизнес‑правило. Важно понимать: all не «ошибается», он честно отвечает на вопрос, который вы задали.

Ошибка №4: использовать count там, где нужна проверка, и забывать про намерение.
Иногда пишут что-то вроде if (xs.count { it < 0 } > 0) ... Это работает, но читается тяжеловато: сначала считаем, потом сравниваем. Если вы по смыслу хотите «есть ли хоть один отрицательный», то any { it < 0 } выразит это проще. count хорош, когда вам реально нужно число.

Ошибка №5: пытаться внутри any/all делать побочные эффекты и потом удивляться логике.
Технически вы можете написать xs.any { println(it); it < 0 }, но получите «странный» эффект: any остановится на первом совпадении и распечатает не все элементы. Это не баг — это короткое замыкание (операции могут завершаться раньше, чем пройдут весь список). Поэтому печать и другие «действия» лучше держать отдельно, а any/all/find/count использовать как чистые вопросы к данным.

Ошибка №6: забывать, что count { ... } проходит по всей коллекции, а find/any/all могут остановиться раньше.
В небольших учебных примерах разницы не видно, но на больших списках может быть заметно: find/any/all часто завершаются на раннем совпадении или нарушении, а count обязан досчитать до конца. Даже если вы пока не думаете про производительность, полезно понимать «почему операция могла быть быстрее» — это помогает писать более осмысленный код.

1
Задача
Kotlin SELF, 22 уровень, 2 лекция
Недоступна
Сканер удачи
Сканер удачи
1
Задача
Kotlin SELF, 22 уровень, 2 лекция
Недоступна
Проверка паролей
Проверка паролей
1
Задача
Kotlin SELF, 22 уровень, 2 лекция
Недоступна
Датчик диапазона
Датчик диапазона
1
Задача
Kotlin SELF, 22 уровень, 2 лекция
Недоступна
Счёт выше порога
Счёт выше порога
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ