JavaRush /Курсы /Kotlin SELF /Создание коллекций

Создание коллекций

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

1. Почему массивов внезапно стало мало

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

Представьте, что вы делаете консольный мини‑учёт расходов (мы будем развивать это приложение дальше по курсу). Сколько будет записей? 5? 50? 500? Вы не знаете. И если вы выберете массив, вы либо ставите огромный размер «на всякий случай», либо начинаете постоянно пересоздавать массив. Оба варианта — так себе: один расходует память и выглядит странно, второй — превращает код в спорт по переноске коробок.

Массив как коробка с пронумерованными ячейками

Массив удобно представлять как коробку с ячейками, у каждой ячейки есть номер (индекс). Коробка создаётся сразу «на N ячеек», и увеличить её нельзя: можно только сделать новую коробку побольше и переложить туда всё содержимое. В Kotlin массивы всегда мутабельны: элемент можно заменить через arr[i] = ..., но размер остаётся прежним. Это важно помнить, потому что «мутабельный массив» — это не «растущий массив».

Вот минимальный пример, который показывает, что массив «держит размер»:


fun main() {
    val arr = arrayOf(1, 2, 3)
    println(arr.size) // 3

    arr[0] = 10
    println(arr[0])   // 10
}

Здесь мы поменяли значение, но размер — как был 3, так и остался.

Как мы «расширяли» массив раньше и почему это раздражает

Когда в массив нужно «добавить элемент», типичный путь — создать новый массив большего размера и скопировать туда старый. Даже если вы делаете это аккуратно, мозг постоянно занят не задачей программы, а бухгалтерией памяти: «сколько мест», «куда копировать», «не забыть индекс».

Пример в стиле «прибавили один элемент» (идея, а не финальный стиль приложения):

fun main() {
    var names = arrayOf("Ann", "Bob")
    println(names.contentToString()) // [Ann, Bob]

    names = names.copyOf(names.size + 1)
    names[names.lastIndex] = "Cara"

    println(names.contentToString()) // [Ann, Bob, Cara]
}

Работает? Да. Удобно? Не очень. И главное: как только таких «добавлений» становится много, код начинает выглядеть так, будто вы пишете не программу учёта расходов, а менеджер по переселению массивов.

2. Коллекции: контейнеры переменного размера

Коллекции — это следующий логичный шаг: контейнеры, которые умеют хранить переменное количество элементов (хоть ноль, хоть тысячу), и при этом дают удобные операции добавления, удаления, поиска.

С коллекциями вы чаще думаете «что я хочу сделать с данными», а не «как вручную переложить данные в новую коробку». Это один из тех моментов, когда язык программирования действительно делает жизнь проще, а не просто добавляет новые слова в ваш словарь.

Три базовые формы: List, Set, Map

Когда говорят «коллекции», обычно имеют в виду три больших семейства: список, множество и карта.

Чтобы было проще выбрать «по смыслу», держите перед глазами такую таблицу:

Тип Что хранит Что важно Повторы Как обычно используют
List<T>
элементы порядок и индекс разрешены список расходов, список задач, история событий
Set<T>
элементы уникальность не хранятся отдельно набор команд, набор категорий, «уже видели»
Map<K, V>
пары ключ → значение доступ по ключу ключи уникальны настройки, словарь, «id → объект», счётчики

Сейчас мы не углубляемся в обходы, сортировки и «красивые операции над коллекциями» — это будет дальше. Наша задача сегодня проще: понять, почему коллекции не равны массивам, чем отличаются List/Set/Map, и научиться их создавать.

Важный нюанс: массивы — это не коллекции

Это тонкость, которая неожиданно спасает от путаницы: в Kotlin массивы считаются отдельной сущностью. В документации даже есть прямое замечание, что массивы — не тип коллекции.

Практически это означает две вещи. Во‑первых, у массива свой набор возможностей и свой набор функций, а у коллекций — свой. Во‑вторых, когда вы читаете код, важно не думать «массив и список — одно и то же, просто по-разному написано». Они похожи внешне (у обоих есть элементы), но логика использования часто различается.

3. Read-only vs mutable: почему Kotlin разделяет «читать» и «менять»

В Kotlin есть важная идея: «тип говорит о том, что с ним можно делать». Поэтому коллекции делятся на read-only интерфейсы и mutable интерфейсы. И это не просто «любовь к усложнению жизни студента», а практичная защита от случайных изменений данных.

В стандартной библиотеке для каждого базового вида коллекции есть пара интерфейсов: read-only (только чтение) и mutable (чтение + изменение). Именно поэтому существуют пары List/MutableList, Set/MutableSet, Map/MutableMap. Выглядит как «два почти одинаковых слова», но смысл у них разный.

Read-only коллекция — это «через эту ссылку нельзя менять»

Read-only тип (List, Set, Map) означает: через эту переменную у вас нет операций изменения (add, remove, присваивания по индексу и т.д.). Вы можете читать размер, брать элементы, проверять наличие, но не «перестраивать контейнер».

Это очень полезно, когда вы хотите явно сказать: «вот здесь мы только читаем». Например, функция получает список расходов и должна лишь посчитать сумму (а не «случайно удалить половину расходов в процессе подсчёта», потому что кто-то перепутал методы).

Пример «только чтение» выглядит так:

fun main() {
    val names: List<String> = listOf("Ann", "Bob", "Ann")
    println(names.size) // 3
    println(names)      // [Ann, Bob, Ann]
}

Здесь names — read-only список. Он может содержать повторы, и порядок элементов сохраняется.

Mutable коллекция — это «можно менять содержимое»

Mutable тип (MutableList, MutableSet, MutableMap) означает: вы можете добавлять, удалять, обновлять элементы.

Самый базовый пример:

fun main() {
    val numbers = mutableListOf(1, 2, 3)
    numbers.add(4)

    println(numbers) // [1, 2, 3, 4]
}

Вот тут происходит магия, ради которой мы и пришли: список вырос без пересоздания коробки.

val и «мутабельность внутри»: частый источник путаницы

Очень важное правило, которое ломает мозг ровно один раз (а потом становится очевидным): val запрещает переназначить переменную, но не запрещает менять объект, на который эта переменная ссылается.

Посмотрите на пример и прочувствуйте разницу:

fun main() {
    val xs = mutableListOf("one", "two", "three")
    xs.add("four")                 // можно
    println(xs)                    // [one, two, three, four]

    // xs = mutableListOf("X")     // нельзя: val нельзя переназначить
}

Это супер‑популярный стиль: «коллекция mutable, но переменная val». Так вы говорите: «контейнер можно менять, но сам контейнер (как объект) не подменяем на другой случайно».

Read-only — это не «абсолютно неизменяемо во всей вселенной»

Ещё один тонкий момент: read-only тип не обещает, что данные вообще никогда не меняются. Он обещает только то, что вы через эту конкретную ссылку не можете вызвать методы изменения.

Если где-то есть другая ссылка на ту же самую mutable-коллекцию, изменения могут происходить «с другой стороны». На этом этапе курса нам не нужно уходить в сложные темы про «разделяемые ссылки», но полезно запомнить интуицию: read-only — это ограничение API, а не волшебный купол неизменяемости.

4. Создание коллекций: listOf, mutableListOf, setOf, mapOf

До сих пор мы создавали массивы через arrayOf(...) или IntArray(...). С коллекциями всё ещё проще: в Kotlin есть фабричные функции, которые создают нужный тип по значениям. Это читается почти как обычный текст: «создай список из этих элементов».

И да, эти функции используются постоянно: вы будете видеть их в примерах, в документации, в реальном коде. Поэтому важно привыкнуть к ним сейчас, пока мы не начали строить логику на коллекциях.

listOf(...) и mutableListOf(...)

listOf(...) создаёт read-only List. Это удобно, когда у вас есть «набор значений», который вы не планируете менять.

mutableListOf(...) создаёт MutableList, который можно расширять и сокращать.

Пример рядом, чтобы сравнить:

fun main() {
    val readOnly = listOf("A", "B", "C")
    println(readOnly)       // [A, B, C]

    val mutable = mutableListOf("A", "B", "C")
    mutable.add("D")
    println(mutable)        // [A, B, C, D]
}

Смысловой совет на будущее (без фанатизма): если не уверены, что нужно менять — начинайте с read-only. Mutable берите тогда, когда изменения реально нужны.

setOf(...) и mutableSetOf(...)

setOf(...) создаёт множество уникальных элементов. Если вы случайно повторите элемент, множество не станет «длиннее», потому что оно про уникальность.

Базовый пример:

fun main() {
    val tags = setOf("kotlin", "kotlin", "jvm")
    println(tags)        // например: [kotlin, jvm]
    println(tags.size)   // 2
}

Заметьте: порядок вывода мы не используем как часть логики. Мы воспринимаем Set как «мешочек уникальных значений».

А вот mutable-вариант:

fun main() {
    val commands = mutableSetOf("add", "list")
    commands.add("remove")
    commands.add("add") // повтор
    println(commands)   // [add, list, remove] (повтор не добавился)
}

mapOf(...), mutableMapOf(...) и инфиксная функция to

Карта (Map) хранит пары «ключ → значение». Ключи уникальны.

Создавать карту удобно через to:

fun main() {
    val prices = mapOf(
        "tea" to 90,
        "coffee" to 150
    )

    println(prices) // {tea=90, coffee=150}
}

С mutableMapOf можно менять и добавлять значения:

fun main() {
    val stock = mutableMapOf("apple" to 3)

    stock["apple"] = 4
    stock["banana"] = 1

    println(stock) // {apple=4, banana=1}
}

Небольшая честность заранее: чтение map[key] может вернуть null, если ключа нет. Это нормальная модель безопасности. Подробно и спокойно мы разберём это в отдельной лекции про Map, а сейчас просто держим в голове как факт, чтобы не удивляться.

5. Пустые коллекции и вывод типа: почему компилятор просит подсказку

Когда вы создаёте непустую коллекцию, Kotlin обычно выводит тип элементов автоматически. Но если коллекция пустая, компилятору «не за что зацепиться»: он не видит ни одного элемента и не может понять, какой тип там должен быть. Поэтому пустые коллекции часто создают с явным указанием типа.

Это важный практический момент: вы будете создавать пустые коллекции постоянно, особенно в приложениях, где данные накапливаются по мере работы программы.

Непустая коллекция: тип выводится сам

Пример, где Kotlin сам понимает тип:

fun main() {
    val a = listOf(1, 2, 3)           // List<Int>
    val b = setOf("kotlin", "jvm")    // Set<String>
    val c = mapOf("a" to 1)           // Map<String, Int>

    println(a) // [1, 2, 3]
    println(b) // [kotlin, jvm]
    println(c) // {a=1}
}

Здесь всё просто: элементы подсказали тип.

Пустая коллекция: тип нужно задать

Теперь создадим пустые коллекции правильно:

fun main() {
    val items = mutableListOf<String>()
    val categories = mutableSetOf<String>()
    val counts = mutableMapOf<String, Int>()

    items.add("milk")
    categories.add("food")
    counts["milk"] = 1

    println(items)      // [milk]
    println(categories) // [food]
    println(counts)     // {milk=1}
}

Обратите внимание: в каждой строке <String> или <String, Int> — это не «лишний шум», а подсказка компилятору, без которой он не обязан угадывать.

6. Мини‑набросок приложения: зачем нам коллекции в реальности

С этого дня курса мы начинаем собирать практическое консольное приложение (без классов и ООП на этом этапе). Чтобы не было ощущения «коллекции ради коллекций», давайте привяжем всё к простой жизненной задаче: хранить вводимые пользователем записи о расходах. Сегодня мы сделаем только самое начало: хранилище в памяти и пару действий. Команды, нормализацию ввода и полноценную логику мы разберём позже — сейчас важно увидеть, что коллекции решают проблему размера.

Представим запись расхода пока просто строкой (вроде "food coffee 300"). Да, это ещё не идеальная модель данных — но на текущем этапе знаний это честный и рабочий временный формат.

Версия на массиве: неудобно и шумно

Вот как выглядел бы «сбор расходов» на массиве (упрощённо):

fun main() {
    var expenses = emptyArray<String>()

    println("Введите расход (или пустую строку для выхода):")
    while (true) {
        val line = readln().trim()
        if (line.isEmpty()) break

        expenses = expenses.copyOf(expenses.size + 1)
        expenses[expenses.lastIndex] = line
    }

    println("Сохранено записей: ${expenses.size}") // например: 2
}

Работает, но ощущение такое, что мы занимаемся не расходами, а переупаковкой данных.

Версия на MutableList: короче, чище, ближе к смыслу

А теперь то же самое на коллекциях:

fun main() {
    val expenses = mutableListOf<String>()

    println("Введите расход (или пустую строку для выхода):")
    while (true) {
        val line = readln().trim()
        if (line.isEmpty()) break

        expenses.add(line)
    }

    println("Сохранено записей: ${expenses.size}") // например: 2
}

Разница не только в количестве строк. Разница в том, о чём вы думаете, пока пишете код. Здесь у вас в голове «добавить запись», а не «пересоздать массив и скопировать». И именно за это коллекции любят в прикладном коде.

Как быстро выбрать контейнер: простая схема для начинающих

В начале знакомства с коллекциями легко зависнуть: «а что выбрать-то? список? множество? карту?». Нормально. Давайте зафиксируем очень простую модель выбора, без погружения в детали производительности и внутренней реализации.

Если вам важен порядок элементов и допускаются повторы, почти всегда это List. Если вам нужна уникальность значений, берите Set. Если вы хотите хранить соответствие «ключ → значение» и доставать по ключу, берите Map.

Можно даже представить это как мини‑блок‑схему:

flowchart TD
    A["Нужно хранить несколько значений"] --> B{"Нужен доступ по ключу?"}
    B -->|Да| M["Map⟨K, V⟩"]
    B -->|Нет| C{"Нужна уникальность?"}
    C -->|Да| S["Set⟨T⟩"]
    C -->|Нет| L["List⟨T⟩"]

Эта схема не «высшая математика», но она помогает быстро принять решение в большинстве бытовых случаев.

7. Типичные ошибки при первом знакомстве с коллекциями

Ошибка №1: путать read-only и immutable («раз нельзя менять через List, значит никто не меняет вообще»).
Read-only типы ограничивают операции через конкретную ссылку, но не гарантируют, что где-то в другом месте нет mutable-ссылки на те же данные. На старте курса достаточно помнить простое правило: read-only — это про API и доступные методы, а не про волшебную неизменяемость мира.

Ошибка №2: держать mutable-коллекцию в var просто «на всякий случай».
Часто новички пишут var list = mutableListOf(...), потому что «ну раз там изменения, значит var». На самом деле изменения происходят методами коллекции (add, remove), а не переназначением переменной. Поэтому удобный и безопасный стиль — val list = mutableListOf(...): так вы случайно не подмените коллекцию другой.

Ошибка №3: создавать пустую коллекцию без типа и удивляться ошибке компиляции.
Когда вы пишете mutableListOf() без элементов, компилятор не обязан угадывать, MutableList чего именно вы хотите. Лекарство простое: указывать тип явно (mutableListOf<String>(), mutableMapOf<String, Int>()) или задавать ожидаемый тип переменной.

Ошибка №4: воспринимать Set как «список без индексов, но в остальном то же самое».
У множества другой смысл: уникальность. Если вы начинаете ждать от Set «первый элемент», «второй элемент», «порядок добавления», вы строите хрупкую логику. На уровне сегодняшней темы мы относимся к Set как к «набору уникальных значений», и этого достаточно.

Ошибка №5: пытаться «обновлять» listOf/setOf/mapOf, как будто они mutable.
Фабрики listOf, setOf, mapOf создают read-only коллекции. Если вы заранее знаете, что будете менять содержимое, выбирайте mutableListOf, mutableSetOf, mutableMapOf. Это не прихоть, а способ сделать намерения кода понятными без комментариев.

Ошибка №6: забывать, что массивы и коллекции — разные сущности.
Иногда новичок видит arrayOf(...), потом видит listOf(...) и думает, что это «одно и то же, просто другое слово». Но массивы — отдельный тип, со своими правилами (например, массивы всегда мутабельны по элементам). Когда вы выбираете контейнер, вы выбираете и стиль работы с данными, поэтому лучше сразу держать различие в голове.

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