1. Навіщо потрібен Set, якщо вже є List
Якщо List — це «черга людей із номерками», то Set — це «список гостей на вході», де важливе лише одне: людина або є в списку, або її немає. У програмуванні така модель трапляється постійно: «дозволені команди», «унікальні теги», «вже оброблені значення», «не додавати повторно». Set допомагає виразити цю ідею напряму, а не через милиці на кшталт «перевірю список перед додаванням».
З погляду Kotlin, Set<T> зберігає унікальні елементи: одне й те саме значення не може існувати всередині множини «у двох примірниках». Також зазвичай кажуть, що порядок у множині не є частиною змісту. Документація формулює це так: Set зберігає унікальні елементи, а їхній порядок «зазвичай не визначено».
Уявіть, що ви робите невеликий консольний застосунок, який зберігає записи витрат (у минулій лекції ми почали це на прикладі списків). У якийсь момент вам майже напевно захочеться:
- зберігати список витрат (там повтори допустимі: каву можна купити 200 разів);
- але водночас тримати унікальний набір категорій, які вже траплялися (щоб показувати підказки або валідовувати введення).
Ось це і є класичне місце для Set.
2. Set і MutableSet: читання та зміни
Коли ви вперше бачите Set і MutableSet, дуже хочеться подумати: «О, Set — це як val, а MutableSet — як var». І ось тут Kotlin підступно усміхається.
Set<T> — це інтерфейс для читання: розмір, перевірка наявності, обхід.
MutableSet<T> — це інтерфейс для змін: додавання, видалення.
Тобто різниця не в тому, чи можна змінити змінну, а в тому, які операції доступні через тип. Цей стиль такий самий, як у List/MutableList.
І ще один важливий момент: mutable-множину майже завжди варто зберігати у val. Тоді ви не зможете випадково «перепривʼязати» змінну до іншої множини, зате зможете чесно змінювати вміст через add/remove. Ця ідея вже звучала в огляді колекцій Kotlin: val захищає посилання, але не забороняє операції запису в mutable-колекції.
Створення множин: setOf, mutableSetOf, порожні множини
Коли ви лише починаєте, найзручніше створювати множини фабричними функціями стандартної бібліотеки. Вони читаються як «створи мені список/множину просто зараз» — без зайвого шуму. До того ж це чудово лягає на звичний стиль із listOf(...) і mutableListOf(...), який ми вже використовували для списків.
setOf(...) і «схлопування» дублів
fun main() {
val tags = setOf("kotlin", "kotlin", "jvm")
println(tags) // [kotlin, jvm]
println(tags.size) // 2
}
Тут важливо не те, у якому порядку надрукується множина, а те, що повтор "kotlin" не створює другого елемента. Set — це про унікальність, а не про «скільки разів трапилося».
Порожня множина: інколи потрібен явний тип
Як і зі списками, порожня колекція часто потребує підказки для компілятора: «а елементи якого типу ви тут очікуєте?».
fun main() {
val usedCategories = mutableSetOf<String>()
usedCategories.add("food")
println(usedCategories) // [food]
}
Якщо ви спробуєте написати просто mutableSetOf() без <String>, Kotlin може чесно відповісти: «Я не знаю, що тут має лежати». І він матиме рацію.
3. Базові операції множини
Унікальність як контракт: add() і повернене Boolean
У списку add() майже завжди означає «елемент точно додався (у кінець)». У множині все цікавіше: add(x) означає «спробуй додати, але якщо такий елемент уже є — нічого не змінюй». Тому операція повертає Boolean: чи вдалося насправді додати новий елемент.
Це дуже корисно, коли ви хочете «повідомити лише один раз» або «не дублювати дію».
fun main() {
val seen = mutableSetOf<Int>()
println(seen.add(10)) // true
println(seen.add(10)) // false
println(seen) // [10]
}
Друга спроба не додала «другу десятку» — вона просто не потрібна за змістом множини.
Перевірка належності: contains() і оператор in
Найприродніша операція для Set — це запитання: «А елемент тут є?». У Kotlin це можна робити двома однаково коректними способами: через contains(x) і через x in set. Другий варіант зазвичай читається приємніше, особливо в умовах.
І так, це одна з причин, чому множину так люблять для перевірок «дозволено/заборонено»: код стає майже розмовним.
fun main() {
val allowedCommands = setOf("add", "list", "remove")
val cmd = "list"
println(cmd in allowedCommands) // true
}
У цьому місці дуже зручно тримати в голові образ: Set — це «набір допустимих значень». Він не про порядок, не про індекси й уже точно не про «видали елемент номер 3».
Видалення: remove() і що воно повертає
Видалення в MutableSet схоже на remove(value) у списку, але за змістом природніше: у множині немає індексів, тому видалення завжди «за значенням». Як і add, воно повертає Boolean: чи вдалося щось видалити.
fun main() {
val usedCategories = mutableSetOf("food", "transport")
println(usedCategories.remove("food")) // true
println(usedCategories.remove("food")) // false
println(usedCategories) // [transport]
}
Другого разу видаляти нічого — "food" уже відсутній.
4. Порядок, рівність і «прибрати дублікати»
Чи можна покладатися на порядок у Set
Інтуїтивно хочеться: «Ну воно ж друкується як "[a, b, c]", отже порядок є». І Kotlin справді часто робить так, що порядок виглядає стабільним. Але тут важливо відокремити «як сьогодні реалізовано» від «який зміст має структура даних».
Формально Set — це множина: унікальні елементи, і порядок «взагалі не головне».
Водночас у стандартній бібліотеці Kotlin реалізація MutableSet за замовчуванням — LinkedHashSet. Вона зберігає порядок вставки, тому first() і last() можуть бути передбачуваними саме в цій реалізації.
Практичне правило для початківців звучить так: ставтеся до Set як до контейнера, де порядок не є контрактом. Якщо ваша логіка розвалюється від перестановки елементів, найімовірніше, вам потрібна інша структура (зазвичай список).
«Прибрати дублікати»: toSet() як швидкий фільтр унікальності
Один із найчастіших прикладних сценаріїв: у вас уже є список (або масив) із повторами, а вам потрібен набір унікальних значень. Наприклад, користувач вводив категорії як завгодно, і ви хочете отримати «список усіх унікальних категорій».
Для цього є toSet(). Вона створює нову колекцію і «схлопує» дублікати. Важливо, що функції копіювання/перетворення на кшталт toSet() роблять знімок на момент виклику. Зміни вихідної колекції потім не «магічно» відображаються в результаті.
fun main() {
val words = listOf("a", "b", "a", "c")
val uniqueWords = words.toSet()
println(uniqueWords) // [a, b, c]
println(uniqueWords.size) // 3
}
Те саме працює і для масивів: масив можна перетворити на Set, і ви побачите, що повтори зникли, а під час перетворення на List — залишаться.
Рівність множин: важливіші елементи, а не порядок
У списків рівність залежить від порядку: [1, 2] не дорівнює [2, 1]. У множин логіка інша: якщо елементи однакові, множини рівні, навіть якщо ви створювали їх «у різному порядку». Це відповідає математичному змісту множини й корисно в реальному житті, коли ви порівнюєте набори дозволів/тегів/фіч.
У документації Kotlin це показано на прикладі: множини рівні, якщо в них ті самі елементи.
fun main() {
val a = setOf(1, 2, 3, 4)
val b = setOf(4, 3, 2, 1)
println(a == b) // true
}
5. Приклад: облік витрат і унікальні категорії
Зараз ми продовжимо наш мініпроєкт на сьогодні: «консольний облік витрат» — без класів і без складної архітектури. Просто щоб набити руку на колекціях. У минулій лекції ми зберігали записи витрат у MutableList<Triple<String, String, Int>>, де поля — це категорія, опис і сума.
Тепер додамо ще одну структуру: MutableSet<String> для унікальних категорій, які вже траплялися. Це дасть нам дві корисні речі: швидку відповідь на запитання «категорія вже була?» і можливість у будь-який момент отримати «список категорій без дублів», не пробігаючи вручну весь список.
Мінісхема даних
Намалюємо дуже просту схему, щоб голова не намагалася тримати все в памʼяті (памʼять — штука дорога, особливо в понеділок уранці):
flowchart LR
Records["records: MutableList⟨Triple⟨Category, Title, Amount⟩⟩"]
Categories["knownCategories: MutableSet⟨Category⟩"]
Records -->|"додали запис"| Categories
Ідея проста: кожен новий запис може «підкинути» нову категорію в множину. Якщо категорія вже була, множина просто не зміниться.
Код: додавання запису й оновлення множини категорій
Тут ми свідомо пишемо невеликі функції й короткі приклади, щоб код читався як підручник, а не як бойовий застосунок (він ще встигне вас наздогнати).
fun addExpense(
records: MutableList<Triple<String, String, Int>>,
knownCategories: MutableSet<String>,
category: String,
title: String,
amount: Int
) {
records.add(Triple(category, title, amount))
knownCategories.add(category)
}
Зверніть увагу: knownCategories.add(category) нічого не ламає, навіть якщо категорія вже була. У цьому й сила множини: вона захищає нас від дублів за контрактом.
Перевірка «нова категорія чи ні» через add()
Тепер використаємо той факт, що add повертає Boolean, щоб вивести користувачу (або самим собі під час зневадження) корисне повідомлення: нова категорія чи ні.
fun addCategoryIfNew(knownCategories: MutableSet<String>, category: String) {
val added = knownCategories.add(category)
if (added) {
println("Нова категорія: $category") // наприклад: Нова категорія: food
} else {
println("Категорія вже була: $category") // наприклад: Категорія вже була: food
}
}
Список так «з коробки» не вміє: у списку вам довелося б окремо робити contains, а потім add — і тут легко помилитися (або просто забути).
Повний мініприклад: записи й категорії
Зберемо невеликий main, який показує ідею повністю. Він не претендує на повноцінний інтерфейс — це навчальна «пісочниця», де ми вручну перевіряємо, що структури даних поводяться так, як і обіцяють.
fun main() {
val records = mutableListOf<Triple<String, String, Int>>()
val knownCategories = mutableSetOf<String>()
addExpense(records, knownCategories, "food", "coffee", 300)
addExpense(records, knownCategories, "transport", "taxi", 1200)
addExpense(records, knownCategories, "food", "lunch", 550)
println(records.size) // 3
println(knownCategories) // [food, transport] (порядок не вважаємо контрактом)
println("food" in knownCategories) // true
}
Тут дуже добре видно: записів три (два з них у категорії "food"), а категорій усього дві — бо множина зберігає унікальні значення.
6. Set і null: так, можна — але обережно
Іноді дані приходять «брудні»: категорія не розпарсилася, користувач увів порожній рядок, а ви поки не навели лад. Оскільки ми вже проходили null-safety, корисно знати: Set може зберігати null, але як унікальний елемент — максимум один null у множині. Документація прямо говорить, що null у Set теж унікальний: другий null не зʼявиться.
Це не заклик зберігати null всюди, де можна (ми не колекціонуємо проблеми), але корисне знання для зневадження та перехідних станів.
fun main() {
val categories: MutableSet<String?> = mutableSetOf()
categories.add(null)
categories.add(null)
println(categories.size) // 1
println(categories) // [null]
}
7. Типові помилки під час роботи з Set і MutableSet
Помилка №1: очікувати, що множина зберігає повтори «як список».
Початківці часто додають одне й те саме значення в MutableSet і дивуються: «Чому розмір не зростає?». Тому що Set за змістом зберігає унікальні елементи. Якщо вам потрібно зберігати «кожне введення користувача», навіть якщо воно повторюється, — це завдання списку. А якщо вам потрібно зберігати «які значення взагалі траплялися» — це завдання множини.
Помилка №2: писати логіку, що залежить від порядку елементів у Set.
Іноді все виглядає так, ніби порядок є, і рука тягнеться зробити «візьму перший елемент і вважатиму його головним». У Kotlin справді часто використовується реалізація, що зберігає порядок вставки, але зміст Set — не в цьому. Щойно ви почнете спиратися на порядок, код стане крихким: зміниться реалізація, зміниться джерело даних — і поведінка несподівано «попливе».
Помилка №3: намагатися звертатися до Set за індексом.
Після List дуже хочеться написати щось на кшталт mySet[0]. Але в множини немає індексів як частини моделі. Це сигнал, що ви обрали не той контейнер під завдання. Якщо вам потрібна позиція — беріть List. Якщо вам потрібні унікальність і перевірки «є/немає» — беріть Set.
Помилка №4: забувати, що add/remove повертають Boolean, і втрачати корисну інформацію.
Багато хто просто викликає add(x) і не дивиться на результат, хоча це майже безплатна підказка про те, чи відбулася зміна. Коли ви валідовуєте введення або ведете список «уже бачених значень», цей Boolean допомагає писати код простіше й зрозуміліше: «якщо додалося вперше — повідом/зроби дію, інакше — пропусти».
Помилка №5: створювати порожнє mutableSetOf() без типу й отримувати помилку компіляції.
Порожня колекція не дає компілятору підказок, тому інколи потрібно явно вказати тип: mutableSetOf<String>(). Це нормально і не робить вас «гіршим програмістом». Радше навпаки: ви явно фіксуєте контракт, а Kotlin перестає вгадувати, що ви мали на увазі.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ