Avatar ·

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

Привет! Я только начал изучать Kotlin после Java и запутался в типах данных. В Java я привык, что массивы — это базовый тип, а списки (List) — это интерфейс из Collections Framework. Но в Kotlin появился еще и тип Array, и я не понимаю, когда какой использовать. Например, я написал такой код: ```kotlin val array: Array = arrayOf(1, 2, 3) val list: List = listOf(1, 2, 3) // Попытка изменить элемент array[0] = 10 // Это работает list[0] = 10 // Ошибка: No set method providing array access ``` Я вижу, что массив изменяемый, а список — нет. Но есть же MutableList! Зачем тогда нужен Array? Кроме того, я слышал, что в Kotlin массивы — это JVM-массивы, а списки — это обертки над ArrayList. Но как это влияет на производительность и выбор? Еще вопрос: я видел, что для примитивных типов есть отдельные массивы: IntArray, ByteArray и т.д. А для List такого нет. Почему? И что мне выбрать для хранения 1000 целых чисел: Array или List? Объясните, пожалуйста, простыми словами, в чем ключевое различие и когда что применять. Примеры кода приветствуются! <<>>

Ключевое различие: неизменяемость размера и тип хранимых данных

Главное различие между Array и List в Kotlin — это неизменяемость размера у массива и тип хранимых данных. Массив имеет фиксированный размер, заданный при создании, а список может динамически расширяться (через MutableList). Но это только верхушка айсберга.

1. Массивы (Array) — низкоуровневый контейнер фиксированного размера

Array в Kotlin — это прямая обертка над JVM-массивом. Его размер никогда не меняется. Вы не можете добавить или удалить элемент — только заменить существующий по индексу.

// Создание массива
val arr = arrayOf(1, 2, 3) // Тип Array<Int>
arr[0] = 10                // ✅ Замена элемента
// arr.add(4)              // ❌ Ошибка — такого метода нет
// arr.removeAt(0)         // ❌ Ошибка

Для примитивных типов (Int, Double, Boolean и т.д.) Kotlin предоставляет специализированные массивы: IntArray, DoubleArray, CharArray и другие. Они хранят значения без упаковки в объекты (без boxing), что экономит память и повышает производительность.

val intArr = IntArray(5) { it * 2 } // [0, 2, 4, 6, 8] — без boxing
val objArr = Array<Int>(5) { it * 2 } // Каждый Int упакован в Integer (boxing)

2. Списки (List) — интерфейс для коллекций с динамическим размером

List — это интерфейс, представляющий упорядоченную коллекцию с произвольным доступом. Реализация по умолчанию — ArrayList (аналог Java). List сам по себе неизменяемый (read-only): вы не можете добавлять, удалять или заменять элементы.

val list = listOf(1, 2, 3) // List<Int>
// list[0] = 10            // ❌ Ошибка: List — read-only
val mutableList = mutableListOf(1, 2, 3) // MutableList<Int>
mutableList[0] = 10        // ✅ Можно заменить
mutableList.add(4)         // ✅ Можно добавить
mutableList.removeAt(0)    // ✅ Можно удалить

Для списков нет специализированных типов для примитивов (как IntList). Все элементы List<Int> будут упакованы в объекты Integer. Это сделано для единообразия работы с коллекциями.

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

  • Используйте Array (или IntArray), когда:
    • Размер коллекции известен заранее и не изменится
    • Вам критична производительность (например, научные расчеты, обработка сигналов)
    • Работаете с большим количеством примитивов (миллионы чисел) — IntArray в разы экономичнее List<Int>
    • Взаимодействуете с Java-кодом, который ожидает массив
  • Используйте List (или MutableList), когда:
    • Размер коллекции может меняться (добавление/удаление элементов)
    • Вам нужны удобные методы: filter, map, find, any и т.д.
    • Храните объекты (не примитивы) — разница в производительности незначительна
    • Пишете общий код, который должен работать с разными реализациями (List — это интерфейс)

4. Пример сравнения производительности

// Array — быстро, но фиксированный размер
val array = IntArray(1_000_000) { it }
var sum = 0L
for (i in array.indices) {
    sum += array[i]
}

// List — медленнее из-за boxing, но гибкий
val list = List(1_000_000) { it }
var sum2 = 0L
for (i in list.indices) {
    sum2 += list[i] // Каждый it упаковывается в Integer
}

5. Итоговая таблица

ХарактеристикаArrayList
РазмерФиксированныйДинамический (MutableList)
ИзменяемостьТолько замена элементовRead-only или mutable
Примитивы без boxingДа (IntArray и др.)Нет
Методы (filter, map)Нет (только extension-функции)Да (богатый API)
ПроизводительностьВысокаяСредняя (из-за boxing)

Совет: В 90% случаев в Kotlin используйте List / MutableList. К ним есть удобные функции из стандартной библиотеки, и код получается более идиоматичным. Массивы оставьте для низкоуровневых задач, работы с большими объемами примитивов или взаимодействия с Java/Android API.

<<>>

Дополнение: vararg и сигнатуры функций

Еще одно важное различие — Array используется в Kotlin для передачи переменного числа аргументов (vararg). Если вы пишете функцию с vararg, Kotlin ожидает именно массив (или распакованные значения):

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

// Вызов с массивом
val arr = arrayOf("a", "b", "c")
printAll(*arr) // Оператор * распаковывает массив

// Вызов со списком — не сработает
val list = listOf("a", "b", "c")
// printAll(*list) // ❌ Ошибка: список нельзя распаковать через *
printAll(*list.toTypedArray()) // ✅ Придется преобразовать

Также помните: Array<Int> и IntArray — это разные типы. Они не взаимозаменяемы:

val a: IntArray = intArrayOf(1, 2, 3)
val b: Array<Int> = arrayOf(1, 2, 3)
// a = b // ❌ Ошибка типа

Поэтому, если API ожидает IntArray (например, в Android для Bitmap.getPixels), используйте именно его, а не Array<Int>.