JavaRush /Курсы /Kotlin SELF /List и

List и MutableList — порядок, индексы, базовые операции

Kotlin SELF
19 уровень , 1 лекция
Открыта

1. List и MutableList: что это и чем отличаются

Если массив в Kotlin — это «коробка фиксированного размера» (как контейнер на складе: сколько ячеек выдали, столько и есть), то List — это «список», который может расти и сжиматься в процессе работы программы. Но главное, что делает List именно списком — это порядок: элементы хранятся в определённой последовательности, и у каждого элемента есть позиция (индекс). Kotlin прямо описывает List как коллекцию с упорядоченным хранением и индексным доступом.

При этом важно помнить тонкость: List — это read-only интерфейс. То есть через ссылку типа List вы можете читать, но не можете менять список. Если же вам нужно добавлять, удалять или заменять элементы, вы берёте MutableList (он расширяет возможности списка операциями записи).

Небольшая опорная таблица:

Тип Можно читать по индексу Можно добавлять/удалять Типичный способ создать
List<T>
да нет
listOf(...)
MutableList<T>
да да
mutableListOf(...)

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: просто чтобы почувствовать, что список живёт

Пока мы не строим полноценное меню команд (это легко превращается в «маленькую операционную систему»), а просто сделаем 3 шага: добавить два товара, распечатать, удалить по номеру, распечатать снова.

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 — они любят подкрадываться ночью и менять местами смысл).

Операция Пример Что делает Что возвращает
Чтение
x = list[i]
Берёт элемент по индексу
T
Замена
list[i] = v
Заменяет элемент по индексу
Unit
Добавить
list.add(v)
Добавляет в конец
Boolean
Удалить по индексу
list.removeAt(i)
Удаляет элемент в позиции
i
удалённый
T
Удалить по значению
list.remove(v)
Удаляет первое совпадение
v
Boolean
Проверить наличие
v in list
Есть ли хотя бы одно
v
Boolean

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 по умолчанию делает код спокойнее: меньше мест, где «вдруг всё переприсвоили», и меньше шансов потерять данные.

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