Collections
Kotlin предоставляет коллекции List, Set и Map, у каждой из которых есть интерфейсы только для чтения и изменяемые интерфейсы. Интерфейс определяет доступные через ссылку операции, но сам по себе не гарантирует неизменяемость.
Основные коллекции
List, Set и Map
List<T>сохраняет порядок элементов, поддерживает доступ по индексу и допускает дубликаты.Set<T>хранит уникальные элементы. Не полагайтесь на порядок обхода, если он не определён конкретной реализацией.Map<K, V>хранит пары ключ-значение с уникальными ключами. Это вид коллекции Kotlin, но он не наследуетCollection.
Выбор зависит от смысла данных. Например, список подходит для упорядоченных элементов UI, множество - для выбранных идентификаторов, а map - для поиска объекта по идентификатору.
List vs MutableList
List<T> - интерфейс только для чтения: он не предоставляет add(), remove() или присваивание по индексу. MutableList<T> расширяет его операциями изменения.
val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable
mutable.add(3)
println(readOnly) // [1, 2, 3]
Доступ только для чтения не означает неизменяемость. Обе ссылки указывают на один объект, поэтому изменения через другую ссылку остаются видимыми. Аналогично, val запрещает переприсвоить переменную, но не изменять саму коллекцию:
Возвращайте List<T>, если вызывающий код не должен менять коллекцию. Если ему также нужен стабильный снимок, создайте защитную копию, например source.toList(), и не сохраняйте или не публикуйте изменяемый псевдоним. Копия остаётся поверхностной: изменяемые элементы внутри неё могут меняться. Для более строгих гарантий существуют персистентные неизменяемые коллекции из отдельной библиотеки.
Операции
Преобразование, отбор и агрегация
Основные операции хорошо выражают намерение:
mapпреобразует каждый элемент.filterоставляет подходящие элементы.flatMapпреобразует элементы в итерируемые значения и объединяет их.foldсворачивает элементы, начиная с заданного аккумулятора.forEachвыполняет побочные эффекты; для создания значений используйте преобразования.
val names = users
.filter { it.isActive }
.map { it.name }
val totalAge = users.fold(0) { total, user -> total + user.age }
Эти операции возвращают результат, не меняя исходную коллекцию. Такие функции, как sort(), сортируют изменяемый список на месте, а sorted() возвращает новый список. В коде управления состоянием это различие должно быть явным.
Цепочки операций над коллекциями выполняются немедленно и могут создавать промежуточные коллекции. asSequence() обрабатывает цепочку лениво и иногда позволяет избежать промежуточных результатов или остановиться раньше, например перед first() или после take(). Но sequence добавляет накладные расходы, поэтому для маленьких коллекций и простых цепочек он не обязательно быстрее. Критичные участки следует измерять.
Практические рекомендации
- Храните изменяемые коллекции внутри их владельца, а наружу передавайте интерфейсы для чтения или снимки.
- Не меняйте коллекцию во время обхода, если API явно не поддерживает такую операцию.
- Не используйте изменяемые объекты как элементы set или ключи map, если могут измениться поля, участвующие в
equals()илиhashCode(). - Предпочитайте операции, ясно выражающие намерение, но используйте цикл, если он понятнее или позволяет избежать лишних выделений памяти в критичном коде.