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, оператор Елвіса ?: одразу робить 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, або задавати поведінку за замовчуванням через оператор Елвіса ?:. Коли ви звикаєте до цього, код стає стійкішим, а «випадкові падіння» зникають як жанр.

Помилка №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 зобовʼязаний дорахувати до кінця. Навіть якщо ви поки що не думаєте про продуктивність, корисно розуміти, «чому операція могла бути швидшою». Це допомагає писати осмисленіший код.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ