1. Оператор []: читання і запис
Тепер перейдемо до головного: як отримати елемент із масиву й як записати нове значення. У Kotlin оператор [] — це стандартний спосіб доступу до елементів масиву: a[i] читає значення, а a[i] = ... записує нове.
Почнемо з найпростішого прикладу: прочитати, змінити, а потім знову прочитати.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
println(steps[0]) // 3000
steps[0] = 7000
println(steps[0]) // 7000
}
Тут важливо помітити дві речі.
По-перше, масиви змінювані: їхні елементи можна змінювати, навіть якщо змінну масиву оголошено як val. val забороняє переприсвоїти саме посилання на масив, але не забороняє змінювати вміст «коробки». Це нормальна поведінка: «коробка та сама, а предмети всередині змінилися».
По-друге, запис steps[0] = 7000 працює лише тоді, коли індекс коректний. Якщо індекс неправильний, програма впаде під час виконання.
2. Індекси та нумерація з нуля
Коли вперше бачите a[0], хочеться запитати: «Чому не a[1]? Я ж людина, а не робот». Відповідь проста: так історично склалося майже в усіх мовах, і Kotlin тут не виняток. Індекс — це зміщення від початку масиву: перший елемент розташований на зміщенні 0. Це незручно рівно перші два дні, а потім ви починаєте рахувати від нуля навіть сторінки в зошиті.
Головне правило для масивів звучить так:
- перший елемент масиву має індекс 0
- останній елемент масиву має індекс size - 1
- допустимі індекси — від 0 до size - 1 включно
Kotlin чітко дотримується цієї моделі: індексація масивів починається з нуля, а доступ і зміна виконуються через оператор [].
Давайте подивимося на коротку ілюстрацію. Уявімо масив із трьох чисел:
значення: [ 50, 10, 99 ]
індекси: 0 1 2
size = 3
останній індекс = size - 1 = 2
Саме тому вираз a[3] для масиву довжини 3 — це спроба звернутися до четвертого елемента, якого не існує.
3. size: скільки елементів і де межа
Якщо в масиві size елементів, виникає спокуса мислити так: «Раз розмір 3, значить індекси 1..3». І от тут Kotlin каже вам «ні-ні»: size — це кількість елементів, а не «максимальний індекс».
Якщо size = 3, то індекси: 0, 1, 2. Порівняймо це в таблиці:
масиву |
Допустимі індекси | Останній індекс |
|---|---|---|
|
немає | — |
|
|
|
|
|
|
|
|
|
До речі, зверніть увагу на size = 0 (порожній масив): у нього немає жодного допустимого індексу. Тому будь-яка спроба зробити a[0] призведе до помилки. Водночас такий масив має право на існування.
Мініприклад:
fun main() {
val a = intArrayOf(10, 20, 30)
println("size = ${a.size}") // size = 3
println("last index = ${a.size - 1}") // last index = 2
}
a.size - 1 — це вираз, який ви будете писати дуже часто. Спочатку в програмах, а потім, здається, і в реальному житті.
4. indices: правильний діапазон індексів
Якщо ви постійно пишете 0 і size - 1, легко помилитися на одиницю. А помилки «на 1» — це давнє прокляття програмування, яке живиться вашими дедлайнами.
Тому Kotlin дає зручну річ: a.indices.
indices — це готовий набір усіх коректних індексів масиву. Якщо масив довжини 3, то indices приблизно відповідає 0..2. Якщо масив порожній, indices буде «порожнім діапазоном», і це теж зручно: багато циклів просто нічого не зроблять.
Найприємніше: indices можна використовувати і в циклах, і в перевірках.
Перевірка індексу через in a.indices
Уявіть, що користувач вводить номер дня тижня, а ви хочете записати кроки в масив. Користувач — істота творча: сьогодні він вводить 3, завтра 999, післязавтра -5 (щоб перевірити «а що буде»). Тож індекс потрібно перевіряти.
fun main() {
val steps = intArrayOf(3000, 5000, 4500) // 3 дні для прикладу
val i = 2
if (i in steps.indices) {
println("steps[$i] = ${steps[i]}") // steps[2] = 4500
} else {
println("Некоректний індекс: $i")
}
}
Ця конструкція читається приблизно так: «якщо i належить до допустимих індексів масиву steps».
І так: in тут — це саме перевірка належності: «чи лежить значення в діапазоні». Ця звʼязка (in + indices) — один із найбазовіших прийомів безпечної роботи з масивами.
Обхід масиву через for (i in a.indices)
Сьогодні ми не заглиблюємося в алгоритми «сума/максимум/пошук». Але сам факт обходу за індексами корисний нам уже зараз, адже інколи потрібен саме індекс, а не лише значення.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
for (i in steps.indices) {
println("Day#$i: ${steps[i]}")
// Day#0: 3000
// Day#1: 5000
// Day#2: 4500
}
}
Чому це краще, ніж for (i in 0..steps.size - 1)? Тому що ви не задаєте межі вручну — і шансів помилитися менше.
Що буде у разі виходу за межі
Є речі, які Kotlin може перевірити заздалегідь (наприклад, «не можна додати String і Int»). А є речі, які залежать від даних під час виконання. Індекси масиву — якраз другий випадок.
Якщо ви пишете:
val a = intArrayOf(1, 2, 3)
println(a[10])
Компілятор може не розпізнати вашу помилку. Можливо, ви потім зміните масив, а можливо, індекс прийде з введення. Тож код скомпілюється, але програма впаде під час виконання з винятком «вихід за межі масиву».
5. Трекер кроків і безпечне оновлення
Щоб приклади були реалістичнішими, вигадаємо невелику навчальну програму, яку можна розвивати далі в межах теми про масиви. Нехай це знову буде консольний трекер кроків за тиждень. Зараз він максимально простий: у нас є масив на 7 днів, ми питаємо в користувача день (індекс) і нове значення кроків та безпечно оновлюємо масив.
Ключовий момент: ми не намагаємося зробити «ідеальний інтерфейс». Також не використовуємо команди й складний розбір введення — лише те, що ви вже знаєте.
fun main() {
val steps = IntArray(7) // за замовчуванням усі елементи = 0
println("Введіть індекс дня (0..6):")
val day = readln().toIntOrNull()
println("Введіть кількість кроків:")
val value = readln().toIntOrNull()
if (day == null || value == null) {
println("Помилка: потрібно вводити цілі числа.")
return
}
if (day in steps.indices) {
steps[day] = value
println("Гаразд: steps[$day] = ${steps[day]}")
} else {
println("Помилка: день має бути від 0 до 6.")
}
}
Зверніть увагу на стиль «подвійного захисту». Спочатку ми перевіряємо, що введення перетворилося на число (toIntOrNull() повернуло не null). Потім перевіряємо, що індекс у межах. Це не параноя, а реальне життя: користувач може ввести що завгодно — зокрема «котики» і -100. А програма має принаймні не падати.
6. Масові операції: fill() і copyOf()
Іноді з масивом потрібно виконати «пакетну» операцію: швидко скинути значення або, навпаки, зберегти копію (знімок) перед змінами. Для цього в Kotlin є прості, але дуже корисні методи.
fill(value): швидко заповнити масив одним значенням
Іноді потрібно масово перезаписати масив: скинути значення, встановити значення за замовчуванням, заповнити «порожніми» даними. Зробити це вручну циклом можна, але нудно: програмістові інколи корисно «лінивіти» конструктивно.
Для цього є метод fill(value): він проходить по масиву й записує одне й те саме значення в усі елементи.
Уявіть сценарій: ми ведемо кроки за тиждень, але хочемо «почати заново» (наприклад, прийняти новий виклик). Тоді найпростіше зробити скидання:
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
steps.fill(0)
for (x in steps) {
print("$x ")
}
// 0 0 0 0 0 0 0
}
Тут важливо не переплутати очікування: fill(0) не змінює розмір масиву, він лише змінює значення елементів.
Якщо ви заповните IntArray(7) сімками, то у вас буде сім елементів, і всі дорівнюватимуть 7 — без жодної магії:
fun main() {
val steps = IntArray(7)
steps.fill(7000)
println(steps[0]) // 7000
println(steps[6]) // 7000
}
Це одна з тих операцій, які роблять код коротшим і зрозумілішим: «заповни масив значенням X» читається краще, ніж цикл на 7 рядків заради тієї ж ідеї.
copyOf(): копія масиву vs «друге імʼя»
Зараз буде одна з найважливіших тем рівня — і водночас одна з найпідступніших для новачків.
Коли ви пишете:
val a = intArrayOf(1, 2, 3)
val b = a
ви не копіюєте масив. Ви створюєте другу змінну, яка вказує на той самий масив. Це як два ярлики на робочому столі, що ведуть в одну й ту саму папку. Видалите файл через один ярлик — і через інший він теж зникне.
А от copyOf() створює новий масив. Спочатку він містить ті самі значення, але існує окремо.
Давайте подивимося на різницю в коді. Спочатку — просто посилання без дублювання масиву:
fun main() {
val original = intArrayOf(10, 20, 30)
val alias = original
alias[0] = 999
println("original[0] = ${original[0]}") // original[0] = 999
println("alias[0] = ${alias[0]}") // alias[0] = 999
}
Багато хто очікує, що original залишиться (10, 20, 30), але ні: alias — це не копія, а той самий масив.
Тепер — правильна незалежна копія:
fun main() {
val original = intArrayOf(10, 20, 30)
val copy = original.copyOf()
copy[0] = 999
println("original[0] = ${original[0]}") // original[0] = 10
println("copy[0] = ${copy[0]}") // copy[0] = 999
}
У трекері кроків це можна використати як «знімок тижня»: ви зберегли старі значення, а потім почали змінювати поточні.
Практика: знімок масиву й скидання тижня
Поєднаємо дві операції (copyOf() і fill()) в один невеликий, але практичний сценарій. Нехай програма робить так: у нас є кроки за тиждень, ми створюємо копію як «архів», а потім скидаємо поточний тиждень у нулі. У реальному житті це може звучати так: «новий тиждень починається з чистого аркуша».
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
val archive = steps.copyOf()
steps.fill(0)
print("archive: ")
for (x in archive) print("$x ")
println()
// archive: 1200 5000 4300 0 8000 6500 2000
print("steps: ")
for (x in steps) print("$x ")
println()
// steps: 0 0 0 0 0 0 0
}
7. Типові помилки
Помилка №1: думати, що останній індекс дорівнює size.
Це класика: масив із 3 елементів, ви пишете a[3] і дивуєтеся, чому програма падає. Правило просте: size — це кількість елементів, а останній індекс — size - 1. Якщо ви не хочете тримати це в голові постійно, використовуйте a.indices для циклів і перевірок, щоб не обчислювати межі вручну.
Помилка №2: не перевіряти індекс, який прийшов із введення (або з розрахунку).
Поки індекс «написаний вручну» і ви впевнені, що він коректний, усе добре. Але щойно індекс приходить від користувача або обчислюється складніше, він може стати відʼємним або надто великим. Тоді звернення a[i] перетворюється на падіння програми. Звичка if (i in a.indices) — це не «зайвий рядок», а страховка від некерованої поведінки.
Помилка №3: вважати, що val b = a створює копію масиву.
Ні, це просто ще одне імʼя для того самого масиву. Змініть b[0] — зміниться і a[0]. Якщо потрібна незалежна копія, використовуйте copyOf(). Це особливо важливо, коли ви зберігаєте «знімки» стану: без copyOf() ви випадково зіпсуєте «архів», змінюючи «поточне».
Помилка №4: намагатися «надрукувати масив» через println(a) і чекати список елементів.
Для новачка це виглядає логічно: «я ж друкую змінну». Але за замовчуванням ви побачите не вміст, а технічне представлення обʼєкта. Найзрозуміліший для поточного рівня спосіб побачити елементи — пройтися циклом і надрукувати кожен елемент. Так, це трохи довше, зате ви точно розумієте, що відбувається.
Помилка №5: очікувати, що fill() змінить розмір масиву або додасть елементи.
fill(value) нічого не додає й не видаляє: він просто перезаписує наявні елементи. Масив залишається фіксованого розміру — рівно таким, яким ви його створили на початку. Якщо хочеться структуру «яка зростає», це буде окрема тема пізніше. А сьогодні тримаймо в голові принцип: масив — це фіксована коробка, і fill() лише розкладає в неї однакові предмети.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ