Введение: «расскажите про коллекции»
«Расскажите, какие коллекции есть в Kotlin» — вопрос, который задают почти на каждом собеседовании, и почти всегда поверхностно. Кандидат перечисляет List, Set, Map — и застревает на первом же уточнении: «а чем List отличается от ArrayList?», «а почему в val список всё-таки меняется?»
Тема коллекций коварная: код с ними пишет каждый, а системного понимания не хватает. В Kotlin у каждой коллекции два интерфейса — read-only и mutable, а на JVM под ними лежат конкретные реализации Java. Разберём по порядку, опираясь на официальную документацию.
List, Set, Map: три базовых интерфейса
Определения из документации простые, но именно их путают:
- List — упорядоченная коллекция с доступом по индексу. Элементы могут повторяться, индексы — от
0доsize - 1. - Set — коллекция уникальных элементов. Дубликаты схлопываются, порядок в общем случае не важен,
nullможет быть только один. - Map — коллекция пар «ключ — значение». Ключи уникальны, значения могут повторяться. Map не наследует
Collection, но считается коллекцией Kotlin.
fun main() {
val list = listOf("one", "two", "one")
println(list[0]) // one
println(list.size) // 3 — дубликаты допустимы
val set = setOf(1, 2, 3, 2, 1)
println(set) // [1, 2, 3] — дубликаты схлопнулись
val map = mapOf("key1" to 1, "key2" to 2)
println(map["key1"]) // 1 — доступ по ключу
}Как не перепутать
Порядок и дубликаты — главные различия. List: порядок важен, дубликаты есть. Set: дубликатов нет, порядок не гарантирован. Map: ключи уникальны, значение одно на ключ.
Обратите внимание на to в mapOf("key1" to 1) — это infix-функция, создающая пару Pair. Для небольших карт это нормально, но в документации честно предупреждают: to создаёт короткоживущий объект Pair, и при строгих требованиях к производительности карту лучше заполнять напрямую — mutableMapOf плюс запись по ключу.
Read-only и mutable: как устроена изменяемость
Ключевая особенность Kotlin, которая отличает его от Java: у каждого типа коллекции два интерфейса. List — read-only (только чтение), MutableList — расширяет его операциями записи. То же для Set/MutableSet и Map/MutableMap.
Важный нюанс, который валит кандидатов: val не делает коллекцию неизменяемой. val защищает ссылку, а не содержимое:
fun main() {
val numbers = mutableListOf(1, 2, 3)
numbers.add(4) // ок: содержимое меняется
numbers[0] = 100 // ок
println(numbers) // [100, 2, 3, 4]
val readOnly: List<Int> = numbers
// readOnly.add(5) — ошибка компиляции: тип List не имеет add
}Тип List — это «обещание не менять». Реализация может быть изменяемой, но через read-only ссылку вы её не троньте — компилятор не даст. Именно поэтому в сигнатурах API принято отдавать List, а принимать MutableList только там, где действительно нужно что-то добавить.
Из этого же следует ковариантность: read-only коллекции ковариантны — List<Rectangle> можно использовать там, где ожидается List<Shape>. С mutable-коллекциями так нельзя: иначе в MutableList<Shape> можно было бы положить Circle, нарушив типы.
Типичная ошибка на собеседовании: «у меня val — значит, коллекция не меняется». Не так: val защищает ссылку, а изменяемость определяет тип — List или MutableList. Смешение этих двух осей («изменяемость ссылки» и «изменяемость содержимого») — главный источник путаницы.
Что лежит под капотом на JVM
Kotlin-коллекции на JVM — это интерфейсы поверх Java-коллекций, а «дефолтные» реализации выбирает стандартная библиотека:
MutableList—ArrayList: изменяемый массив, быстрый доступ по индексу.MutableSet—LinkedHashSet: сохраняет порядок вставки при обходе.MutableMap—LinkedHashMap: тоже сохраняет порядок вставки.
Проверяется это одной строкой:
fun main() {
val list = mutableListOf("a", "b", "c") // на JVM это ArrayList
val set = mutableSetOf("a", "b", "c") // LinkedHashSet
val map = mutableMapOf("a" to 1) // LinkedHashMap
println(list::class) // class java.util.ArrayList
println(set::class) // class java.util.LinkedHashSet
println(map::class) // class java.util.LinkedHashMap
}Альтернативы существуют и нужны осознанно:
HashSet/HashMap— не дают никаких гарантий порядка, но, как отмечает документация, требуют меньше памяти на то же количество элементов.LinkedList— двусвязный список: вставка и удаление по позиции быстрые, а вот доступ по индексу обходит список от ближайшего конца. В Kotlin-коде встречается редко: дефолтныйArrayListпочти всегда лучше.
Создавать коллекции можно не только через listOf/mutableListOf. Полезно знать конструктор с инициализатором и строители, которые возвращают read-only результат:
fun main() {
val doubled = List(3) { it * 2 } // [0, 2, 4]
println(doubled)
val result: List<Int> = buildList {
add(1)
add(2)
add(3)
}
println(result) // [1, 2, 3]
val empty = emptyList<String>() // пустая коллекция с явным типом
}Подводный камень: listOf() без типа не скомпилируется — «пустую» коллекцию нужно объявлять с явным типом: val empty = emptyList<String>(). А buildList, buildSet, buildMap — удобный способ собрать коллекцию и вернуть её read-only: внутри строителя работаете с mutable-версией, наружу отдаёте неизменяемый интерфейс.
Равенство и копирование
Коллекции сравниваются структурно: две коллекции равны, если у них одинаковые элементы и одинаковое поведение — в List важна позиция, в Set и Map порядок не имеет значения.
fun main() {
val listA = listOf(1, 2, 3)
val listB = listOf(1, 2, 3)
println(listA == listB) // true
val setA = setOf(1, 2, 3)
val setB = setOf(3, 2, 1)
println(setA == setB) // true — порядок не важен
val mapA = mapOf("a" to 1, "b" to 2)
val mapB = mapOf("b" to 2, "a" to 1)
println(mapA == mapB) // true — пары те же
}Для сравнения используется обычный == — внутри он вызывает equals. Это удобно в тестах и в data-классах.
Ещё один нюанс — копирование. toList(), toSet(), toMutableList() создают новую коллекцию — снимок на момент вызова. Изменения исходной коллекции на снимок не влияют (это поверхностная копия: сами элементы — те же объекты):
fun main() {
val source = mutableListOf(1, 2, 3)
val copy = source.toList()
source.add(4)
println(source) // [1, 2, 3, 4]
println(copy) // [1, 2, 3] — снимок не изменился
}Это часто всплывает в коде: передали куда-то «копию», а потом удивляются, почему список вырос. Если нужен именно снимок — toList(), если нужно, чтобы изменения были видны — передавайте ссылку как есть.
Ошибки на собеседовании
Соберу промахи, которые я вижу на собеседованиях чаще всего.
List— порядок и дубликаты;Set— уникальность;Map— пары «ключ — значение».valне делает коллекцию неизменяемой — изменяемость определяет тип (ListvsMutableList).- На JVM дефолты:
ArrayList,LinkedHashSet,LinkedHashMap. HashMap/HashSet— без гарантий порядка, но экономнее по памяти.- Равенство коллекций — структурное:
listA == listBпо элементам. toList()— снимок;buildList— read-only результат из mutable-строителя.
Ошибка-классика: «Set хранит элементы в порядке добавления, поэтому first() вернёт первый добавленный». Это правда только для дефолтного LinkedHashSet. Как только кто-то передаёт HashSet — порядок исчезает, и код на first() становится непредсказуемым. Рассчитывать на порядок в Set — антипаттерн.
Две оси изменяемости
val/var — изменяемость ссылки, List/MutableList — изменяемость содержимого. Кандидат, который разделяет эти оси, отвечает на тему коллекций без запинок — и заодно показывает понимание дизайна языка.
На мок-собеседованиях я часто прошу написать функцию, которая принимает List, и спрашиваю: «что будет, если сюда передадут mutableListOf?». Ответ — «ничего страшного: тип параметра read-only, через него менять не дадут» — показывает понимание, а не заученный текст.
Готовитесь к переходу на middle?
Бесплатная диагностика: разберу ваши пробелы и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.
Итоги
Коллекции — одна из тех тем, где «знаю, потому что пользуюсь» не работает: собеседование требует системного ответа.
- Интерфейсы: List (порядок, дубликаты), Set (уникальность), Map (ключи уникальны) — у каждого read-only и mutable версия.
- Изменяемость:
valзащищает ссылку; содержимое меняется черезMutable*-интерфейсы, а read-only тип — это контракт. - JVM: дефолтные реализации —
ArrayList,LinkedHashSet,LinkedHashMap;HashMap/HashSet— альтернатива без гарантий порядка.
Главное
Двухслойная структура — интерфейсы Kotlin поверх реализаций JVM — это то, что отличает уверенный ответ от «ну, List — это список». Назовите оба слоя, и вопрос закрыт.
Если коллекции, дженерики и другие базовые темы языка всплывают на каждом собеседовании зияющими пробелами — не нужно готовиться в одиночку. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрыть в первую очередь.