1. List і MutableList: що це та чим відрізняються
Якщо масив у Kotlin — це «коробка фіксованого розміру» (як контейнер на складі: скільки комірок видали, стільки й є), то List — це «список», який може рости й стискатися під час роботи програми. Але головне, що робить List саме списком, — це порядок: елементи зберігаються в певній послідовності, і кожен елемент має позицію (індекс). Kotlin прямо описує List як колекцію з упорядкованим зберіганням та індексним доступом.
Водночас важливо памʼятати нюанс: List — це read-only інтерфейс (лише для читання). Тобто через посилання типу List ви можете читати, але не можете змінювати список. Якщо ж вам потрібно додавати, видаляти або замінювати елементи, беріть MutableList (він розширює можливості списку операціями запису).
Невелика опорна таблиця:
| Тип | Можна читати за індексом | Можна додавати/видаляти | Типовий спосіб створити |
|---|---|---|---|
|
так | ні | |
|
так | так | |
2. Індекси та доступ до елементів
Порядок та індекси: чому перший елемент — це 0
Коли ви працюєте зі списком, ви майже завжди подумки кажете «перший», «другий», «останній». Компʼютер же каже «0-й», «1-й», «2-й»… і так далі. Індекси в Kotlin починаються з нуля й ідуть до lastIndex, де lastIndex = size - 1. Це не «особливість Kotlin», а загальний підхід у більшості мов.
Корисно тримати в голові коротку схему:
size = 4
індекси: 0 1 2 3
^ ^
перший lastIndex
І ось тут зʼявляється класична помилка новачків: спроба звернутися до list[size] як до «останнього елемента». На жаль, size — це кількість елементів, а не останній індекс. Останній індекс — size - 1 або lastIndex.
Мініприклад:
fun main() {
val xs = listOf(10, 20, 30, 40)
println(xs.size) // 4
println(xs.lastIndex) // 3
}
Читання елементів: list[i] і get(i)
Коли ви вже прийняли філософію «індекси починаються з нуля», наступний крок — навчитися діставати елемент. У Kotlin є дві форми, які роблять те саме: list[i] і list.get(i). Першу зручніше читати, а друга інколи корисна, коли ви хочете підкреслити, що це саме виклик методу.
Приклад:
fun main() {
val words = listOf("one", "two", "three", "four")
println(words[0]) // one
println(words.get(2)) // three
println(words[3]) // four
}
Якщо індекс неправильний (наприклад, -1 або size), програма впаде з помилкою виходу за межі. Це нормально: список захищає себе від прохання «дістань мені елемент із паралельного всесвіту».
3. Базові операції MutableList
Заміна елемента за індексом
Коли ви переходите від List до MutableList, магія починається: ми можемо змінювати елементи. Найпростіша операція — замінити значення в наявній позиції. Синтаксис виглядає так, ніби ви працюєте з масивом: list[i] = newValue.
Усередині це аналог set(i, newValue), але запис із [] сприймається природніше.
Приклад:
fun main() {
val items = mutableListOf("a", "b", "c")
items[1] = "B" // замінили елемент з індексом 1
println(items) // [a, B, c]
}
Тут важливий зміст: ми замінили елемент на позиції 1. Ми нічого не «вставляли»: розмір не змінювався, інші елементи не зсувалися.
Додавання елементів: add(value)
Тепер поговорімо про зростання списку. У реальних задачах ви майже завжди починаєте з порожнього списку й поступово наповнюєте його: користувач вводить задачі, додає товари в кошик, записує витрати, збирає імена учасників — що завгодно.
MutableList.add(value) додає елемент у кінець списку. Це одна з найчастіших операцій, і її зручно сприймати як «дописати в кінець».
Приклад:
fun main() {
val numbers = mutableListOf(1, 2, 3)
numbers.add(4)
numbers.add(5)
println(numbers) // [1, 2, 3, 4, 5]
println(numbers.size) // 5
}
Якщо ви відчуваєте легку радість від того, що size зростає, вітаю: ви щойно пережили «перше дорослішання контейнера». Масив так не вміє.
Видалення за індексом: removeAt(index) і ефект зсуву
Видалення — це місце, де індекси починають поводитися як індекси. Тобто після видалення одного елемента всі елементи праворуч «зʼїжджають ліворуч», і їхні індекси змінюються. Це логічно: якщо з черги людей пішла одна людина, усі, хто стоїть за нею, стають ближчими до початку.
removeAt(index) видаляє елемент у позиції index і повертає видалений елемент. Це зручно: ви можете одразу вивести, що саме видалили, або використати значення далі.
Приклад:
fun main() {
val items = mutableListOf("x", "y", "z")
val removed = items.removeAt(0)
println(removed) // x
println(items) // [y, z]
}
Візуальна схема зсуву:
було: [ x, y, z ]
індекси 0 1 2
removeAt(0)
стало: [ y, z ]
індекси 0 1
Якщо ви зберігаєте «важливі номери» елементів (наприклад, користувач вибрав «видалити елемент № 2»), то після видалення номери решти змінюються. Це не баг — це математика.
Видалення за значенням: remove(value)
У MutableList є ще один спосіб видалення: remove(value). На відміну від removeAt, він працює не з позицією, а з самим значенням: «видали елемент "bread"».
Тут зʼявляються дві важливі ідеї.
Перша: remove(value) повертає Boolean. true — якщо видалення відбулося, false — якщо такого значення не було (тобто список не змінився).
Друга: якщо однакових значень кілька, то remove(value) видаляє перший знайдений збіг, а не «всі одразу».
Приклад:
fun main() {
val products = mutableListOf("milk", "bread", "tea", "bread")
val ok1 = products.remove("bread")
println(ok1) // true
println(products) // [milk, tea, bread]
val ok2 = products.remove("coffee")
println(ok2) // false
println(products) // [milk, tea, bread]
}
Зауважте: "bread" був двічі, а після видалення залишився один. Це нормально — ми видалили лише перший збіг.
Перевірка наявності: contains(value) і оператор in
Коли ви працюєте зі списками, часто виникає питання «а це вже є?». Наприклад: «цей товар уже в кошику?», «така команда підтримується?», «цю задачу вже додано?».
Можна писати list.contains(value), а можна — читабельніше — value in list. Це одне й те саме за змістом; просто другий варіант ближчий до людської мови.
Мініприклад:
fun main() {
val allowed = listOf("add", "list", "remove")
println(allowed.contains("list")) // true
println("add" in allowed) // true
println("exit" in allowed) // false
}
Список при цьому може містити повтори (на відміну від Set), а contains / in кажуть лише «є хоча б один такий елемент чи ні». Kotlin підкреслює, що List допускає дублікати та зберігає порядок елементів.
4. Практика: «Список покупок v0»
Зараз ми зробимо невеликий фрагмент застосунку, який зростатиме разом із курсом. Поки що — без складних команд і без архітектури: просто подивимося, як MutableList допомагає зберігати дані, а користувацьке введення — додавати й видаляти.
Зробимо модель максимально простою: список рядків MutableList<String>, де кожен рядок — це товар.
Друк списку «по-людськи»
Почнімо з функції друку. Ми вже вміємо працювати з циклами, тож просто переберемо товари й покажемо їх із номерами. Зауважте: номер для людини починається з 1, а індекс у списку — з 0. Тому «людський номер = індекс + 1».
fun printShoppingList(items: List<String>) {
println("Список покупок:")
for (i in 0..items.lastIndex) {
println("${i + 1}. ${items[i]}")
}
}
Якщо список порожній, 0..items.lastIndex виглядатиме дивно (наприклад, 0..-1). Щоб уникнути сюрпризів, додамо простий захист.
fun printShoppingList(items: List<String>) {
if (items.isEmpty()) {
println("Список покупок порожній.") // Список покупок порожній.
return
}
println("Список покупок:")
for (i in 0..items.lastIndex) {
println("${i + 1}. ${items[i]}")
}
}
Додавання товару: add(...) і trim()
Тепер напишемо функцію, яка читає рядок, прибирає зайві пробіли й додає його до списку. Тут ми використовуємо знайомий підхід: «прочитав → підготував → використав».
fun addItem(items: MutableList<String>) {
print("Введіть товар: ")
val raw = readln()
val name = raw.trim()
if (name.isEmpty()) {
println("Порожня назва — не додаю.") // Порожня назва — не додаю.
return
}
items.add(name)
println("Додано: $name") // наприклад: Додано: milk
}
Видалення за номером: removeAt(index) і перетворення «номер → індекс»
Тепер зробимо видалення за номером, який вводить користувач (1, 2, 3, …). Нам потрібно перетворити номер на індекс: index = number - 1. І обовʼязково перевірити межі — інакше привіт, помилка часу виконання.
fun removeItemByNumber(items: MutableList<String>) {
print("Введіть номер товару для видалення: ")
val number = readln().trim().toIntOrNull()
if (number == null) {
println("Це не число.") // Це не число.
return
}
val index = number - 1
if (index < 0 || index > items.lastIndex) {
println("Немає товару з номером $number.") // Немає товару з номером 10.
return
}
val removed = items.removeAt(index)
println("Видалено: $removed") // наприклад: Видалено: milk
}
Міні-main: просто щоб відчути, що список живе
Поки ми не будуємо повноцінне меню команд (це легко перетворюється на «маленьку операційну систему»), а просто зробимо три кроки: додамо два товари, роздрукуємо список, видалимо за номером, а потім роздрукуємо ще раз.
fun main() {
val items = mutableListOf<String>()
addItem(items)
addItem(items)
printShoppingList(items)
removeItemByNumber(items)
printShoppingList(items)
}
Якщо ви запустите це й спробуєте введення, ви відчуєте головне: MutableList — це контейнер, який зручно змінювати, а операції add/removeAt роблять рівно те, що написано на коробці.
5. Корисні нюанси та шпаргалка
val для MutableList — це нормально
На цьому етапі багато хто робить логічну (і дуже людську) помилку: «Раз список змінюється, значить змінна має бути var». Насправді найчастіше змінна має бути val, тому що ви не збираєтеся перепризначати посилання на інший список. Ви плануєте змінювати вміст поточного списку.
Приклад «правильної нормальності»:
fun main() {
val items = mutableListOf("a", "b")
items.add("c")
items[0] = "A"
println(items) // [A, b, c]
}
val забороняє зробити так:
items = mutableListOf("x") // не можна, але нам зазвичай і не треба
Зате він не заважає викликати add, removeAt, remove і змінювати елементи. Це як «адреса будинку постійна, але меблі всередині можна переставляти».
Таблиця-шпаргалка з операцій MutableList
Іноді корисно мати компактну шпаргалку, щоб не плутати методи (особливо remove і removeAt — вони люблять підкрадатися вночі й міняти місцями зміст).
| Операція | Приклад | Що робить | Що повертає |
|---|---|---|---|
| Читання | |
Бере елемент за індексом | |
| Заміна | |
Замінює елемент за індексом | |
| Додати | |
Додає в кінець | |
| Видалити за індексом | |
Видаляє елемент у позиції |
видалений |
| Видалити за значенням | |
Видаляє перший збіг |
|
| Перевірити наявність | |
Чи є хоча б одне |
|
6. Типові помилки під час роботи з List і MutableList
Помилка № 1: плутати size і lastIndex.
Дуже хочеться написати list[list.size] й очікувати «останній елемент», бо «size же про кінець». Але size — це кількість елементів, а індекси йдуть від 0 до size - 1 (тобто lastIndex). Уникнути помилки просто: «останній елемент» — це list[list.lastIndex], а не list[list.size].
Помилка № 2: намагатися змінювати List, створений через listOf.
listOf(...) дає read-only List. Він чудово підходить для «списку констант» або «тимчасового набору значень». Але якщо далі вам потрібні add/remove, беріть mutableListOf(...) і тип MutableList.
Помилка № 3: переплутати removeAt(index) і remove(value).
Це класика рівня «взяв не той ключ від дверей». removeAt(2) видаляє елемент у позиції 2, а remove("2") (якби ви так написали) намагався б видалити значення "2". Рятує звичка: якщо в назві є At, значить працюємо «за місцем», тобто за індексом.
Помилка № 4: забути, що після removeAt елементи зсуваються.
Новачки інколи зберігають індекс, видаляють елемент, а потім намагаються звернутися до «наступного» за старим індексом — і дивуються дивній поведінці. Після видалення список змінюється: усі елементи праворуч «переїжджають» на індекс менше, і це нормально. Якщо ви пишете код, який видаляє кілька елементів поспіль, потрібно памʼятати про цей зсув і діяти обережно.
Помилка № 5: вважати, що remove(value) видалить усі збіги.
Коли у списку є дублікати, remove(value) видаляє лише перший збіг. Це відповідає ідеї списку: порядок важливий, а повтори допустимі. Якщо ви очікували «видали все», то ви очікували іншу операцію (але сьогодні ми її не вводимо — спочатку навчимося впевнено керувати базовою механікою).
Помилка № 6: робити var items = mutableListOf(...) «бо список змінюється».
Зазвичай val краще: ви змінюєте вміст, а не «перекидаєте» змінну на новий список. var потрібен, якщо ви справді плануєте замінити весь список цілком. Дисципліна val за замовчуванням робить код спокійнішим: менше місць, де «раптом усе переприсвоїли», і менше шансів утратити дані.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ