Коллекции. Общий обзор

Стандартная библиотека Kotlin предоставляет большой набор инструментов для работы с коллекциями — группами с переменным количеством элементов (или нулём элементов), которые используются для решения какой-либо задачи.

Коллекции — это общая концепция для большинства языков программирования, поэтому если вы знакомы с коллекциями, например, в Java или Python, то можете пропустить данное введение и перейти к разделам с более подробным описанием.

Обычно в коллекции находится несколько объектов одного типа (но также коллекция может быть пустой). Эти объекты называются элементами или items. Например, все студенты одного факультета образуют коллекцию, которую можно использовать для расчёта их среднего возраста.

Типы коллекций в Kotlin:

  • List (список) - упорядоченная коллекция, в которой к элементам можно обращаться по индексам — целым числам, отражающим положение элементов в коллекции. Идентичные элементы (дубликаты) могут встречаться в списке более одного раза. Примером списка является предложение: это группа слов, их порядок важен, и они могут повторяться.
  • Set (множество) - коллекция уникальных элементов. Отражает математическую абстракцию множества: группа объектов без повторов. Как правило, порядок расположения элементов здесь не имеет значения. Примером множества является алфавит.
  • Map (словарь, ассоциативный список) - набор из пар "ключ-значение". Ключи уникальны и каждый из них соответствует ровно одному значению. Значения могут иметь дубликаты. Ассоциативные списки полезны для хранения логических связей между объектами, например, ID сотрудников и их должностей.

Kotlin позволяет управлять коллекциями независимо от того, какой именно тип объектов в них хранится: будь то String, Int или какой-то собственный класс, общие принципы работы с коллекцией всегда неизменны. Стандартная библиотека Kotlin предоставляет общие интерфейсы, классы и функции для создания, заполнения и управления коллекциями любого типа.

Интерфейсы коллекций и связанные с ними функции находятся в пакете kotlin.collections. Давайте рассмотрим его содержимое.

Типы коллекций

Стандартная библиотека Kotlin предоставляет реализации для основных типов коллекций: Set, List, Map. Есть два вида интерфейсов, предоставляющих каждый из этих типов:

  • неизменяемый ( read-only) - предоставляет операции, которые дают доступ к элементам коллекции.
  • изменяемый (mutable) - расширяет предыдущий интерфейс и дополнительно даёт доступ к операциям добавления, удаления и обновления элементов коллекции.

Обратите внимание, что изменяемую коллекцию не требуется объявлять с помощью ключевого слова var. Связано это с тем, что изменения вносятся в изначальные объекты коллекции без изменения ссылки на саму коллекцию. Но если вы объявите коллекцию с помощью val и попытаетесь ее перезаписать, то получите ошибку компиляции.

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four")
    numbers.add("five") // this is OK    
    //numbers = mutableListOf("six", "seven") // compilation error
}

Неизменяемые типы коллекций ковариантны. Это означает, что если класс Rectangle наследуется от Shape, вы можете использовать List<Rectangle> там, где требуется List<Shape>. Другими словами, типы коллекций имеют такое же отношение подтипов, что и типы элементов. Map-ы ковариантны по типу значения, но не по типу ключа.

В свою очередь, изменяемые коллекции не являются ковариантными; в противном случае это привело бы к сбоям во время выполнения. Если MutableList<Rectangle> был подтипом MutableList<Shape>, вы могли добавить в него других наследников Shape (например, Circle), таким образом нарушая изначальный тип коллекции - Rectangle.

Ниже представлена ​​схема интерфейсов коллекций Kotlin:

Пройдемся по интерфейсам и их реализациям.

Collection

Collection<T> является корнем в иерархии коллекций. Этот интерфейс представляет собой обычное поведение неизменяемой коллекции: операции типа size, get и т. д. Collection наследуется от интерфейса Iterable<T>, который определяет операции для итерации элементов. Вы можете использовать Collection как параметр функции, которая может работать с разными типами коллекций. Для более конкретных случаев следует использовать наследников Collection: List и Set.

fun printAll(strings: Collection<String>) {
        for(s in strings) print("$s ")
        println()
    }

fun main() {
    val stringList = listOf("one", "two", "one")
    printAll(stringList) // one two one

    val stringSet = setOf("one", "two", "three")
    printAll(stringSet) // one two three
}

MutableCollection<T> - это Collection с операциями записи, такими как add и remove.

fun List<String>.getShortWordsTo(shortWords: MutableList<String>, maxLength: Int) {
    this.filterTo(shortWords) { it.length <= maxLength }
    // throwing away the articles
    val articles = setOf("a", "A", "an", "An", "the", "The")
    shortWords -= articles
}

fun main() {
    val words = "A long time ago in a galaxy far far away".split(" ")
    val shortWords = mutableListOf<String>()
    words.getShortWordsTo(shortWords, 3)
    println(shortWords) // [ago, in, far, far]
}

List

List<T> хранит элементы в определённом порядке и обеспечивает к ним доступ по индексу. Индексы начинаются с нуля (0 - индекс первого элемента) и идут до lastIndex, который равен (list.size - 1).

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println("Number of elements: ${numbers.size}") // 4
    println("Third element: ${numbers.get(2)}") // three
    println("Fourth element: ${numbers[3]}") // four
    println("Index of element \"two\" ${numbers.indexOf("two")}") // 1
}

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

data class Person(var name: String, var age: Int)

fun main() {
    val bob = Person("Bob", 31)
    val people = listOf(Person("Adam", 20), bob, bob)
    val people2 = listOf(Person("Adam", 20), Person("Bob", 31), bob)
    println(people == people2) // true
    bob.age = 32
    println(people == people2) // false
}

MutableList<T> - это List с операциями записи, специфичными для списка, например, для добавления или удаления элемента в определённой позиции.

fun main() {
    val numbers = mutableListOf(1, 2, 3, 4)
    numbers.add(5)
    numbers.removeAt(1)
    numbers[0] = 0
    numbers.shuffle()
    println(numbers) // [4, 0, 3, 5]
}

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

По умолчанию в Kotlin реализацией List является ArrayList, который можно рассматривать как массив с изменяемым размером.

Set

Set<T> хранит уникальные элементы; их порядок обычно не определён. null также является уникальным элементом: Set может содержать только один null. Два множества равны, если они имеют одинаковый размер и для каждого элемента множества есть равный элемент в другом множестве.

fun main() {
    val numbers = setOf(1, 2, 3, 4)
    println("Number of elements: ${numbers.size}") // Number of elements: 4
    if (numbers.contains(1)) println("1 is in the set")

    val numbersBackwards = setOf(4, 3, 2, 1)
    println("The sets are equal: ${numbers == numbersBackwards}") // true
}

MutableSet - это Set с операциями записи из MutableCollection.

По умолчанию реализацией Set является LinkedHashSet, который сохраняет порядок вставки элементов. Следовательно, функции, которые зависят от порядка элементов, такие как first() или last(), возвращают предсказуемые результаты для таких множеств.

fun main() {
    val numbers = setOf(1, 2, 3, 4)  // по умолчанию LinkedHashSet
    val numbersBackwards = setOf(4, 3, 2, 1)

    println(numbers.first() == numbersBackwards.first()) // false
    println(numbers.first() == numbersBackwards.last()) // true
}

Альтернативная реализация - HashSet - не сохраняет порядок элементов, поэтому при вызове функций first() или last() вернётся непредсказуемый результат. Однако HashSet требует меньше памяти для хранения того же количества элементов.

Map

Map<K, V> не является наследником интерфейса Collection; однако это один из типов коллекций в Kotlin. Map хранит пары "ключ-значение" (или entries); ключи уникальны, но разные ключи могут иметь одинаковые значения. Интерфейс Map предоставляет такие функции, как доступ к значению по ключу, поиск ключей и значений и т. д.

fun main() {
    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

    println("All keys: ${numbersMap.keys}") // [key1, key2, key3, key4]
    println("All values: ${numbersMap.values}") // [1, 2, 3, 1]
    if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")    
    if (1 in numbersMap.values) println("The value 1 is in the map")
    if (numbersMap.containsValue(1)) println("The value 1 is in the map") // аналогичен предыдущему условию
}

Две Map-ы, содержащие равные пары, равны независимо от порядка пар.

fun main() {
    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)    
    val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

    println("The maps are equal: ${numbersMap == anotherMap}") // The maps are equal: true
}

MutableMap - это Map с операциями записи, например, можно добавить новую пару "ключ-значение" или обновить значение, связанное с указанным ключом.

fun main() {
    val numbersMap = mutableMapOf("one" to 1, "two" to 2)
    numbersMap.put("three", 3)
    numbersMap["one"] = 11

    println(numbersMap) // {one=11, two=2, three=3}
}

По умолчанию реализацией Map является LinkedHashMap - сохраняет порядок элементов. Альтернативная реализация - HashMap - не сохраняет порядок элементов.