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() лишь раскладывает в неё одинаковые предметы.

1
Задача
Kotlin SELF, 7 уровень, 2 лекция
Недоступна
Датчик температуры
Датчик температуры
1
Задача
Kotlin SELF, 7 уровень, 2 лекция
Недоступна
Первый и последний
Первый и последний
1
Задача
Kotlin SELF, 7 уровень, 2 лекция
Недоступна
Просмотр очков
Просмотр очков
1
Задача
Kotlin SELF, 7 уровень, 2 лекция
Недоступна
Сброс попыток
Сброс попыток
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ