Коллекции Котлина | Кодементор

Узнайте о коллекциях в Kotlin в этом посте Марко Девчича, автора Краткое руководство по Kotlin.

Коллекции Kotlin были вдохновлены Scala и его изменяемыми и неизменяемыми типами коллекций. Kotlin определяет свои типы коллекций в пакете kotlin.collections, и там вы увидите, что он в основном переопределяет типы, которые уже существуют в Java. Но все они компилируются в Java-версию того же типа. Таким образом, вам не нужно беспокоиться о каких-либо дополнительных накладных расходах при их использовании.

Интерфейс Iterable является основой для всех типов коллекций. У него есть только один метод, итератор, который возвращает объект, который может выполнять итерацию по коллекции:

public interface Iterable<out T> {
public operator fun iterator(): Iterator<T>
}

Объект Iterator знает, как перебирать коллекцию. У него есть два метода: next, который возвращает следующий элемент, и hasNext, который сообщает, есть ли в коллекции еще элементы:

public interface Iterator<out T> 
{
public operator fun next(): T
public operator fun hasNext(): Boolean
}

Следующий интерфейс в иерархии — Коллекция. Он добавляет еще пару функций в интерфейсы Iterable, такие как размер коллекции и способ проверить, содержится ли элемент внутри коллекции:

public interface Collection<out E> : Iterable<E> {
public val size: Int
public fun isEmpty(): Boolean
public operator fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}

Следующими в иерархии интерфейсов являются List и Set. Оба они расширяют интерфейс Collection. Список — это индексированная коллекция, и с помощью метода get вы можете получить элемент из коллекции по его положению:

public interface List<out E> : Collection<E> {
override val size: Int

override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>

override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

public operator fun get(index: Int): E

public fun indexOf(element: @UnsafeVariance E): Int

public fun lastIndexOf(element: @UnsafeVariance E): Int

public fun listIterator(): ListIterator<E>

public fun listIterator(index: Int): ListIterator<E>

public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

Набор — это неиндексированная коллекция уникальных элементов, что можно увидеть в следующей команде:

public interface Set<out E> : Collection<E> {
override val size: Int

override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>

override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}

Существует также интерфейс Map, который представляет собой набор пар ключ-значение. Карта не расширяет интерфейсы Collection и Iterable:

public interface Map<K, out V> {
public val size: Int

public fun isEmpty(): Boolean

public fun containsKey(key: K): Boolean

public fun containsValue(value: @UnsafeVariance V): Boolean

public operator fun get(key: K): V?

@SinceKotlin("1.1")
@PlatformDependent
public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
return null as V
}

public val keys: Set<K>

public val values: Collection<V>

public val entries: Set<Map.Entry<K, V>>

public interface Entry<out K, out V> {
public val key: K
public val value: V
}
}

Теперь все эти интерфейсы присутствуют в Java в пакете java.util. Но есть одно большое отличие: в Kotlin все они доступны только для чтения, то есть неизменяемы. Kotlin различает изменяемые и неизменяемые коллекции. Каждый из этих интерфейсов в Kotlin также имеет изменяемую версию, включая MutableIterator и MutableList.

В следующей таблице представлены типы Mutable и Immutable с их представлением Java:

Захват.PNG

Изменяемые интерфейсы расширяют неизменяемые и добавляют методы для изменения коллекций. Изменяемые версии считаются равными своим аналогам в Java.
Типы, с которыми вы будете работать, в основном реализуют интерфейсы List, Set или Map, и теперь мы рассмотрим их более подробно.

Списки

Списки — это наборы индексированных элементов. Списки поддерживают вставку и извлечение элементов по их положению. Помимо класса ArrayList, который, вероятно, является наиболее часто используемой реализацией, интерфейс List также реализован в классе LinkedList.

Обычно вы будете использовать класс ArrayList, потому что он предлагает извлечение элементов по положению за постоянное время и не требует дополнительных затрат памяти на дополнительный объект Node для каждого элемента, который есть в LinkedList. Библиотека Kotlin предоставляет несколько методов для создания экземпляров интерфейсов List. Вы можете создать неизменяемый экземпляр списка, вызвав функцию listOf. Функция принимает параметр vararg, который можно использовать для инициализации списка элементами:

val superHeros = listOf("Batman", "Superman", "Hulk")

Также есть функция arrayListOf:

val moreSuperHeros = arrayListOf("Captain America", "Spider-Man")

Если вы смотрите на реализацию этих методов, вы можете подумать, что они избыточны, поскольку оба они возвращают экземпляр класса ArrayList. Классы имеют одинаковое имя, но немного разную реализацию. Тот, который возвращается из функции listOf, является закрытым классом, определенным в пакете java.util.Arrays. Это то же самое, что вы получили бы, если бы вызвали функцию Arrays.asList в Java. Функция arrayListOf возвращает «настоящий» класс ArrayList из пакета java.util.

Если вы хотите создать изменяемую версию списка, есть функция mutableListOf:

val superHeros = mutableListOf("Thor", "Daredevil", "Iron Man")

Это можно изменить следующим образом, если в этой версии определен метод добавления:

superHeros.add("Wolverine")

Существует также метод emptyList, который возвращает список только для чтения без элементов:

val empty = emptyList<String>()

Наборы

Набор — это набор уникальных элементов. У наборов нет гарантий заказа, поэтому вы не можете получить предмет по его положению. В стандартной библиотеке Java есть несколько реализаций интерфейса Set. Вероятно, наиболее часто используемым является класс HashSet. Он может добавлять, удалять и проверять, содержится ли элемент в постоянное время. Класс LinkedHashSet имеет примерно такую ​​же производительность, но внутри он использует LinkedList для хранения элементов. Это связано с дополнительными затратами памяти на дополнительный объект Node для каждого содержащегося в нем элемента, но имеет преимущество предсказуемого порядка итераций.

Когда класс HashSet повторяется, он находится в неопределенном порядке, другими словами, не в том порядке, в котором элементы были вставлены. Существует также класс TreeSet, который упорядочивает элементы в соответствии с их естественным порядком (или в соответствии с порядком, который вы указываете в интерфейсе Comparable). Он хранит свой элемент внутри двоичного дерева, и это дает TreeSet немного худшую производительность, чем две другие реализации (логарифмическая вместо постоянной) для добавления, получения и проверки наличия элемента.

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

val superHeros = setOf("Batman", "Superman")
val superHeros2 = hashSetOf("Thor", "Spider-Man")
val superHeros3 = linkedSetOf("Iron-Man", "Wolverine")
val sortedNums = sortedSetOf(3,10,12,4,9)
val empty = emptySet<String>()

Если вам нужен изменяемый набор, есть только одна функция, mutableSetOf:

val mutableSet = mutableSetOf("Hulk", "DareDevil")

Карты

Карты — это коллекции, которые связывают ключи со значениями. Они имеют аналогичные реализации для наборов. Существует наиболее распространенный класс HashMap, реализация, сохраняющая порядок вставки при повторении; LinkedHashMap; и отсортированная версия, TreeMap. Производительность этих классов такая же, как и у их братьев и сестер Sets. В стандартной библиотеке Kotlin есть следующие функции для инициализации неизменяемых карт:

val map = mapOf(1 to "one", 2 to "two")
val hashMap = hashMapOf(3 to "three", 4 to "four")
val linkedMap = linkedMapOf(5 to "five", 6 to "six")
val sortedMap = sortedMapOf(7 to "seven", 8 to "eight")
val empty = emptyMap<Int, String>()

Все эти функции принимают переменные аргументы типа Pair для инициализации карт. Мы использовали функцию расширения to для создания пар. Это функция из стандартной библиотеки:

/**
 * Creates a tuple of type [Pair] from this and [that].
 *
 * This can be useful for creating [Map] literals with less noise, for example:
 * @sample samples.collections.Maps.Instantiation.mapFromPairs
*/
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

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

val mutableMap = mutableMapOf(9 to "nine", 10 to "ten")

Индексация

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

val superHeros = mutableListOf("Superman", "Batman")
val superman = superHeros[0]

И вот как вы бы установили элемент в определенной позиции:

superHeros[2] = "Thor"

В случае карт вы указываете ключ в квадратных скобках, и возвращается соответствующее значение:

val map = mutableMapOf(1 to "one", 2 to "two")
val two = map[2]

И если вы хотите что-то вставить в карту, вы можете поместить ключ в квадратные скобки, а затем присвоить этому ключу значение:

map[3] = "three"

Типы платформ и неизменяемые коллекции

Функции Kotlin, такие как нулевая безопасность, применяются во время компиляции, и Kotlin пытается обеспечить (отличную) совместимость с Java. Когда код Java вызывает ваш код Kotlin, он уже скомпилирован, и компилятор Kotlin не может применять свои проверки на null. Именно по этой причине команда разработчиков Kotlin представила концепцию типа платформы. Проще говоря, тип платформы — это тип, определенный в Java (или любом другом языке JVM).

В случае обнуляемости код Kotlin может обрабатывать их как обнуляемые или необнуляемые. Если во время выполнения нулевое значение передается переменной, не допускающей значение NULL, среда выполнения Kotlin выдает исключение нулевого указателя.

Аналогично обрабатываются неизменяемые типы Kotlin Collection. Помните, что Kotlin не определяет свой собственный тип Collection, вместо этого он использует типы из Java. Поскольку в Java нет одинаковой концепции изменяемых и неизменяемых коллекций (в Java есть неизменяемые коллекции с определенными типами, такими как UnmodifiableList), код Kotlin должен рассматривать их как изменяемые или неизменяемые. Это не то, о чем вам обычно нужно беспокоиться.

Но если вы общаетесь в другом направлении, т.е. вызываете Java из Kotlin, то вам нужно быть немного осторожнее. Взгляните на следующую функцию Java:

public void checkStrings(List<String> strings) {
for (String s : strings) {
// do something
}
strings.add("Item added in Java");
}

Мы можем представить, что он выполняет некоторую обработку или проверку строк, а затем добавляет элемент в список. И мы можем передать ему неизменяемый список из Kotlin следующим образом:

val strings = listOf("Item added in Kotlin", "Another item added in Kotlin")
// UnsupportedOperationException thrown here
java.checkStrings(strings)

Но вызов этой функции Java вызовет исключение UnsupportedOperationException во время выполнения. Причина в том, что функция listOf создает конкретную реализацию интерфейса List, которая расширяет класс AbstractList и не переопределяет метод добавления из него. Базовая реализация метода добавления AbstractList просто генерирует исключение UnsupportedOperationException. В подобных случаях, когда вы знаете, что код Java изменяет коллекцию, всегда передайте какую-либо версию изменяемой версии из Kotlin.

Теперь, когда вы знаете, как типы коллекций Kotlin используют соответствующие типы Java и как Kotlin обеспечивает неизменяемость коллекций во время компиляции, легко обмануть компилятор и изменить неизменяемую коллекцию с помощью простого приведения типов. Рассмотрим этот пример, где мы объявляем неизменяемую переменную списка, присваиваем ей экземпляр класса ArrayList, а затем с помощью простого приведения мы можем добавить к нему больше элементов:

val list: List<String> = ArrayList()
val mutableList = (list as ArrayList).add("string")

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

Надеюсь, вам понравилось читать эту статью. Чтобы узнать больше о языке Kotlin, ознакомьтесь с Краткое руководство по Kotlinбыстрое практическое руководство, которое знакомит вас с основными функциями Kotlin, чтобы вы могли начать разработку бесшовных приложений.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *