Avatar ·

В чем разница между List и Array в Kotlin? Когда что использовать?

Привет, коллеги! Я только начал изучать Kotlin и перешел с Java. В Kotlin есть два похожих типа: `List` и `Array`. Я вижу, что оба хранят несколько элементов, но не могу понять, в чем ключевая разница и когда какой использовать. Например, я пишу простую программу для хранения имен студентов: ```kotlin val namesArray = arrayOf("Alice", "Bob", "Charlie") val namesList = listOf("Alice", "Bob", "Charlie") ``` Оба работают, я могу обращаться по индексу: `namesArray[0]` и `namesList[0]` — оба возвращают "Alice". Но я заметил, что `Array` имеет свойство `size`, а `List` — `count()`. Также я могу изменить элемент массива: ```kotlin namesArray[0] = "Eve" // работает namesList[0] = "Eve" // ошибка компиляции ``` Почему так? И еще я слышал, что `List` может быть изменяемым (`MutableList`), а массив всегда изменяемый. Но зачем тогда нужен `Array`, если `MutableList` делает то же самое? И как это связано с производительностью? Подскажите, пожалуйста, в чем фундаментальная разница и какой тип выбрать для обычных задач? Заранее спасибо! <<>>

Ключевые отличия: List (неизменяемый/изменяемый) vs Array

Отличный вопрос! Это одна из самых частых путаниц у новичков в Kotlin. Давайте разложим всё по полочкам.

Главное различие — тип и семантика:

  • Array — это низкоуровневая структура данных, представляющая собой фиксированную последовательность элементов одного типа в памяти. Это прямой аналог массивов в Java (int[], String[]).
  • List — это интерфейс (часть Kotlin Collections Framework), который представляет собой упорядоченную коллекцию. Он может иметь разные реализации (например, ArrayList, LinkedList).

Основные различия в деталях

1. Изменяемость (Mutability)

Массивы всегда изменяемы — вы можете поменять значение любого элемента по индексу. Однако вы не можете изменить размер массива после создания.

Интерфейс List по умолчанию неизменяемый (read-only) — у него нет методов add, remove или сеттера. Для изменяемой коллекции используйте MutableList:

val immutableList: List<String> = listOf("a", "b")
// immutableList[0] = "c" // Ошибка!

val mutableList: MutableList<String> = mutableListOf("a", "b")
mutableList[0] = "c" // Работает
mutableList.add("d")  // Работает

2. Типизация и ковариантность

Это важный момент для дженериков. Array<T> инвариантен в Kotlin. Это означает, что Array<String> не является подтипом Array<Any>. Почему? Потому что массив можно изменять (записывать значения), и это может привести к ошибкам типов во время выполнения.

А вот List<T> ковариантен (объявлен как interface List<out T>). Поэтому List<String> является подтипом List<Any> — это безопасно, так как вы не можете записывать в него (нет методов для изменения):

val strArray: Array<String> = arrayOf("a")
// val anyArray: Array<Any> = strArray // Ошибка компиляции!

val strList: List<String> = listOf("a")
val anyList: List<Any> = strList // Работает!

3. Производительность и тип данных

Для примитивных типов (Int, Double, Boolean и т.д.) Kotlin предоставляет специальные классы массивов для избежания автоупаковки (boxing): IntArray, DoubleArray, BooleanArray и т.д. Они работают как int[] в Java — очень быстро и без лишних объектов.

val intArray = intArrayOf(1, 2, 3) // Эффективно, без Integer объектов
val intList = listOf(1, 2, 3)      // Каждый Int упакован в Integer

List<Int> всегда использует объекты Integer, что может быть критично для больших массивов данных.

Когда что использовать?

  • Используйте Array, если:
    • Вам нужна фиксированная длина (например, для буфера или технической структуры).
    • Критична производительность и вы работаете с примитивами (IntArray и т.д.).
    • Вы вызываете Java-код, который ожидает массив.
    • Вы работаете с аннотациями или vararg (например, fun foo(vararg args: String)).
  • Используйте List (по умолчанию), если:
    • Вам нужны гибкие коллекции с возможностью добавления/удаления элементов (используйте MutableList).
    • Вы хотите использовать богатый API коллекций: filter, map, sortedBy и т.д.
    • Вам нужна ковариантность (например, передавать список подтипов как список родительского типа).
    • Вы пишете современный Kotlin-код — в 90% случаев List является правильным выбором.

Итог: Для большинства повседневных задач (список пользователей, товаров, задач) используйте List / MutableList. Массивы оставьте для низкоуровневых операций, работы с примитивами или совместимости с Java.

<<>>

Дополнение: Специальные случаи и идиомы Kotlin

Хочу добавить несколько практических моментов, которые помогут вам избежать ошибок.

1. Преобразование между Array и List

Вы легко можете конвертировать одно в другое, если это необходимо:

val array = arrayOf(1, 2, 3)
val listFromArray: List<Int> = array.toList()

val list = listOf(4, 5, 6)
val arrayFromList: Array<Int> = list.toTypedArray()

2. Размер и проверка на пустоту

У массивов есть свойство size, у списков — size (не count(), хотя count() тоже работает как расширение). Для проверки на пустоту используйте isEmpty() — он есть у обоих типов.

3. Важный нюанс с vararg

Если функция принимает vararg, вы можете передать массив со специальным оператором * (spread operator), но список так передать нельзя без конвертации:

fun printAll(vararg items: String) { items.forEach { println(it) } }

val arr = arrayOf("A", "B")
printAll(*arr) // Работает

val lst = listOf("C", "D")
// printAll(*lst) // Ошибка!
printAll(*lst.toTypedArray()) // Так можно

4. Идиоматический Kotlin

В сообществе Kotlin принято использовать List везде, где это возможно. Если вы видите в чужом коде Array без веской причины (производительность, совместимость с Java), скорее всего, это запах кода. Начните с listOf() или mutableListOf() и переходите к массивам только когда точно знаете, зачем они вам нужны.