Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Массивы.md
T

12 KiB
Raw Blame History

title, description, tags
title description tags
Массивы Array, специализированные IntArray, сравнение через contentEquals и почему обычно нужен List
kotlin
stdlib

Массив - фиксированная по размеру последовательность элементов. В Kotlin это класс Array<T>, а не синтаксическая конструкция.

val a = arrayOf(1, 2, 3)
val b = Array(3) { it * 2 }          // [0, 2, 4] — по лямбде от индекса
val c = arrayOfNulls<String>(3)      // [null, null, null]
val d = emptyArray<Int>()

println(a[0])          // 1
a[0] = 10
println(a.size)        // 3
println(a.joinToString())  // 10, 2, 3

Размер задаётся при создании и не меняется. Элементы изменяемы всегда - даже если массив объявлен через val.

Специализированные массивы

Для чисел есть отдельные классы без упаковки:

val ints = intArrayOf(1, 2, 3)
val zeros = IntArray(5)              // [0, 0, 0, 0, 0] — заполнен нулями
val squares = IntArray(5) { it * it } // [0, 1, 4, 9, 16]
val doubles = doubleArrayOf(1.0, 2.0)
val chars = charArrayOf('a', 'b')
val bools = BooleanArray(3)          // [false, false, false]
Класс Байт-код Аналог в Java
IntArray int[] int[]
LongArray long[] long[]
DoubleArray double[] double[]
CharArray char[] char[]
BooleanArray boolean[] boolean[]
Array<Int> Integer[] Integer[]

Разница принципиальна: Array<Int> хранит упакованные объекты, IntArray - примитивы. В горячем коде это заметно по памяти и скорости.

val boxed: Array<Int> = arrayOf(1, 2, 3)      // Integer[] — аллокация на элемент
val raw: IntArray = intArrayOf(1, 2, 3)       // int[] — без упаковки
println("${boxed.size} ${raw.size}")

Сравнение - только contentEquals

== на массивах сравнивает ссылки, а не содержимое. Это главная ловушка:

val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a == b)                 // false — разные объекты!
println(a.contentEquals(b))     // true  — так правильно
println(a.contentHashCode())    // хеш по содержимому

Для вложенных массивов - глубокие версии:

val x = arrayOf(arrayOf(1), arrayOf(2))
val y = arrayOf(arrayOf(1), arrayOf(2))
println(x.contentEquals(y))         // false — сравнивает ссылки вложенных
println(x.contentDeepEquals(y))     // true

Печать

toString() массива бесполезен:

val a = arrayOf(1, 2, 3)
println(a)                       // [Ljava.lang.Integer;@1b6d3586
println(a.contentToString())     // [1, 2, 3]
println(a.joinToString())        // 1, 2, 3
println("$a".startsWith("[L"))   // true — шаблон вызывает тот же toString

Внутри строкового шаблона всегда нужен contentToString() или joinToString().

Обход

val a = arrayOf("x", "y", "z")
for (item in a) print(item)              // xyz
for (i in a.indices) print(a[i])         // xyz
for ((i, v) in a.withIndex()) print("$i$v ")  // 0x 1y 2z
a.forEach { print(it) }                  // xyz
a.forEachIndexed { i, v -> print("$i$v ") }
println()
println(a.lastIndex)                     // 2

Операции

Массивы поддерживают те же операции, что коллекции:

val a = intArrayOf(3, 1, 2)
println(a.sorted())              // [1, 2, 3] — возвращает List!
println(a.sortedArray())         // новый IntArray
a.sort()                         // сортировка на месте
println(a.contentToString())     // [1, 2, 3]
println(a.sum())                 // 6
println(a.average())             // 2.0
println(a.max())                 // 3
println(a.filter { it > 1 })     // [2, 3] — List
println(a.map { it * 2 })        // [2, 4, 6] — List
println(a.reversed())            // [3, 2, 1] — List
println(a.reversedArray().contentToString())  // [3, 2, 1]
println(a.contains(2))           // true
println(2 in a)                  // true
println(a.indexOf(2))            // 1

Обратите внимание: filter, map, sorted, reversed возвращают List, а не массив. Версии, сохраняющие тип, имеют суффикс Array.

Копирование и изменение размера

Размер массива неизменен, поэтому «добавление» - это создание нового:

val a = intArrayOf(1, 2, 3)
println((a + 4).contentToString())           // [1, 2, 3, 4] — новый массив
println(a.copyOf().contentToString())        // [1, 2, 3]
println(a.copyOf(5).contentToString())       // [1, 2, 3, 0, 0] — дополнен нулями
println(a.copyOf(2).contentToString())       // [1, 2] — обрезан
println(a.copyOfRange(1, 3).contentToString()) // [2, 3]
println(a.sliceArray(0..1).contentToString())  // [1, 2]
a.fill(0)
println(a.contentToString())                 // [0, 0, 0]

Преобразование в коллекции и обратно

val a = arrayOf(1, 2, 3)
val list: List<Int> = a.toList()
val set = a.toSet()
val back: Array<Int> = list.toTypedArray()
val raw: IntArray = list.toIntArray()
println("${list} ${set} ${back.size} ${raw.size}")

Многомерные массивы

Отдельного типа нет - это массив массивов:

val grid = Array(3) { IntArray(4) }          // 3x4, заполнен нулями
grid[1][2] = 5
println(grid[1].contentToString())           // [0, 0, 5, 0]

val table = Array(2) { r -> IntArray(2) { c -> r * 2 + c } }
println(table.joinToString { it.contentToString() })  // [0, 1], [2, 3]

Строки могут иметь разную длину (рваный массив):

val jagged = arrayOf(intArrayOf(1), intArrayOf(1, 2, 3))
println(jagged.joinToString { it.size.toString() })   // 1, 3

Когда массив, а когда List

Массив нужен редко. Используйте его, если:

  • важна производительность и нужны примитивы без упаковки (IntArray)
  • работаете с API, требующим массив (vararg, Java-библиотеки)
  • размер фиксирован и известен заранее

В остальных случаях - List: он не даёт менять себя случайно, имеет больше операций и корректный equals.

val list = listOf(1, 2, 3)
println(list == listOf(1, 2, 3))   // true — работает из коробки

Подробнее: Коллекции

vararg

Параметр с переменным числом аргументов внутри функции - массив:

fun sum(vararg nums: Int): Int {
    var total = 0
    for (n in nums) total += n     // nums это IntArray
    return total
}

fun main() {
    println(sum(1, 2, 3))          // 6
    println(sum())                 // 0
    val arr = intArrayOf(1, 2)
    println(sum(*arr))             // 3 — spread-оператор
}

Подробнее: Функции

[!info] Отличия от Java

Java Kotlin
int[] a = {1,2,3} val a = intArrayOf(1,2,3)
new int[5] IntArray(5)
a.length a.size
Arrays.toString(a) a.contentToString()
Arrays.equals(a,b) a.contentEquals(b)
Arrays.deepEquals(a,b) a.contentDeepEquals(b)
Arrays.sort(a) a.sort()
Arrays.copyOf(a,n) a.copyOf(n)
Arrays.fill(a,0) a.fill(0)
int[] и Integer[] IntArray и Array<Int>
f(arr) для varargs f(*arr) — нужен spread

Статические методы Arrays стали методами массива - находятся автодополнением, а не поиском по документации.

Разделение IntArray / Array<Int> сделано намеренно: в Java Integer[] и int[] тоже разные, но это легко не заметить. В Kotlin разные имена не позволяют спутать упакованный массив с примитивным.

Массивы в Kotlin инвариантны, в Java - ковариантны. Java разрешает Object[] o = new String[1]; o[0] = 1; и падает в рантайме (ArrayStoreException). В Kotlin Array<Any> = Array<String> не скомпилируется. Подробнее: Вариантность

Сравнить: Массивы в Java

[!warning] Подводные камни == на массивах сравнивает ссылки. Самая частая ошибка:

val a = arrayOf(1, 2)
println(a == arrayOf(1, 2))              // false
println(a.contentEquals(arrayOf(1, 2)))  // true

По этой же причине массив нельзя использовать как ключ Map или в Set - hashCode считается по ссылке.

Массив внутри data class ломает equals:

data class Packet(val data: ByteArray)

fun main() {
    val p1 = Packet(byteArrayOf(1))
    println(p1 == Packet(byteArrayOf(1)))   // false!
}

Сгенерированный equals сравнивает массив по ссылке, а не по содержимому. Поэтому в data class с массивом equals/hashCode пишут руками. Подробнее: Data class

println(array) печатает мусор вида [Ljava.lang.Integer;@1b6d3586. То же в строковых шаблонах.

val не защищает элементы:

val a = intArrayOf(1, 2)
a[0] = 99          // разрешено
println(a.contentToString())   // [99, 2]

Неизменяемость даёт List, а не val.

Array<Int> вместо IntArray - скрытая просадка. Каждый элемент упакован в Integer: больше памяти и промахи кеша.

arrayOfNulls и Array(n) для непримитивных типов требуют разного подхода:

val a = arrayOfNulls<String>(3)        // Array<String?>
val b = Array(3) { "" }                // Array<String> — инициализатор обязателен
println("${a.size} ${b.size}")

Создать Array<String> без инициализатора нельзя - компилятор не позволит оставить non-null элементы пустыми.

Дополнительно: Коллекции Далее: Функции