JavaRush /Курси /Kotlin SELF /Індекси й безпечний доступ у масивах: [], size, indices, ...

Індекси й безпечний доступ у масивах: [], size, indices, fill, copyOf

Kotlin SELF
Рівень 7 , Лекція 2
Відкрита

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
немає
1
0
0
2
0, 1
1
3
0, 1, 2
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() лише розкладає в неї однакові предмети.

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