배열

배열

배열(array)은 같은 타입 또는 그 서브타입의 값을 고정된 개수만큼 담는 데이터 구조예요. 배열의 요소는 순서를 가지고 인덱스로 접근해요. Kotlin은 Array<T> 클래스와 원시 타입 배열을 제공해요.

출처: Kotlin 공식 문서

본문

배열은 언제 쓸까

배열은 Java API와의 상호 운용이나 저수준 요구 사항을 다룰 때 사용해요. 예를 들어 일반 애플리케이션에서 요구하는 수준 이상의 성능 요구가 있거나, 커스텀 데이터 구조를 직접 만들어야 할 때요.

대부분의 사용 사례에서는 배열 대신 컬렉션을 사용하는 걸 권장해요.

기능 배열(Array) 컬렉션(Collection)
크기 고정 타입에 따라 다름
읽기 전용 변형 없음, 항상 변경 가능 있음 (ListSet)
요소 추가·제거 기본 지원 없음. 새 배열에 할당하고 복사해야 함 있음 (가변 컬렉션)
==로 구조적 동일성 아니요, 참조를 비교함. .contentEquals()를 대신 사용
원시 값 원시 타입 배열은 박싱 없이 값을 저장 보통 박싱됨
Java 상호 운용 T[]에 매핑 java.util.Listjava.util.Set에 매핑
함수형 스타일 필터·변환 제한적 광범위

배열을 컬렉션으로 변환하는 방법도 알아두면 좋아요.

배열 만들기

배열을 만들려면 다음을 사용할 수 있어요.

참고: val로 배열을 선언하는 것은 변수에 다시 할당하는 것만 막을 뿐, 내용을 읽기 전용으로 만들지는 않아요. 요소는 val 배열이든 var 배열이든 변경 가능해요. 그 차이는 참조에만 적용될 뿐 내용에는 영향이 없어요. 읽기 전용 뷰가 필요하다면 컬렉션을 사용하세요.

값이 있는 배열

알려진 값의 집합으로 타입 있는 배열을 만들려면 arrayOf() 함수를 써요. Kotlin이 타입을 자동으로 추론해요.

fun main() {
    val simpleArray = arrayOf(1, 2, 3) // Array<Int>
    println(simpleArray.joinToString())
    // 1, 2, 3
}

빈 배열

요소가 없는 배열을 만들려면 emptyArray() 함수를 써요. 요소의 타입은 할당문의 왼쪽이나 오른쪽에서 지정할 수 있어요.

val emptyArrayRight = emptyArray<String>()
val emptyArrayLeft: Array<String> = emptyArray()

배열에 요소를 추가하는 방법도 알아두세요.

null로 채운 배열

주어진 크기의 배열을 null 요소로 채워 만들려면 arrayOfNulls() 함수를 사용해요.

fun main() {
    val nullArray: Array<Int?> = arrayOfNulls(3)
    println(nullArray.joinToString())
    // null, null, null
}

Array 생성자

Array 생성자는 배열 크기와 배열 요소의 값을 반환하는 함수를 받아요.

fun main() {
    val zeroes = Array<Int>(3) { 0 }
    println(zeroes.joinToString())
    // 0, 0, 0

    val squares = Array(5) { i -> i * i }
    println(squares.joinToString())
    // 0, 1, 4, 9, 16
}

중첩 배열

중첩(nested) 또는 다차원 배열을 만들려면 배열의 배열을 사용해요. 중첩 배열은 타입이나 크기가 같을 필요가 없어요.

fun main() {
    // 2차원 배열을 만들어요
    val twoDArray = Array(2) { Array<Int>(2) { 0 } }
    println(twoDArray.contentDeepToString())
    // [[0, 0], [0, 0]]

    // 3차원 배열을 만들어요
    val threeDArray = Array(3) { Array(3) { Array<Int>(3) { 0 } } }
    println(threeDArray.contentDeepToString())
    // [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]]]
}

원시 타입 배열

Array 클래스를 원시 값과 함께 쓰면 컴파일러가 이 값들을 객체로 박싱해요. 이 박싱 오버헤드를 피하려면 전용 원시 타입 배열을 사용할 수 있어요. 이 배열들은 Array<T> 클래스의 서브클래스는 아니지만, 비슷한 함수와 프로퍼티 집합을 제공해요.

Kotlin 타입 Java 동등 타입
BooleanArray boolean[]
ByteArray byte[]
CharArray char[]
DoubleArray double[]
FloatArray float[]
IntArray int[]
LongArray long[]
ShortArray short[]

참고: Kotlin에는 전용 StringArray 타입이 없어요. String은 원시 타입이 아니므로, 타입을 추론한 arrayOf() 또는 arrayOf<String>() 함수를 사용하세요.

원시 타입 배열을 만들려면 다음 옵션 중 하나를 사용해요.

  • 생성자 함수:
fun main() {
    // 크기 5의 Int 배열을 값 0으로 초기화해 만들어요
    val primitiveTypeArray = IntArray(5)
    println(primitiveTypeArray.joinToString())
    // 0, 0, 0, 0, 0

    // Int 배열을 만들고 초기화 함수를 받아요
    val squares = IntArray(5) { i -> i * i }
    println(squares.joinToString())
    // 0, 1, 4, 9, 16
}
  • 팩토리 함수:
fun main() {
    // 5개 요소를 가진 Int 배열을 만들어요
    val numbers = intArrayOf(1, 2, 3, 4, 5)
    println(numbers.joinToString())
    // 1, 2, 3, 4, 5

    // 3개 요소를 가진 Char 배열을 만들어요
    val characters = charArrayOf('K', 't', 'l')
    println(characters.joinToString())
    // K, t, l

    // 3개 요소를 가진 Double 배열을 만들어요
    val doubles = doubleArrayOf(0.22, 4.16, 0.5)
    println(doubles.joinToString())
    // 0.22, 4.16, 0.5
}

참고: 원시 타입 배열을 객체 타입 배열로 변환하려면 .toTypedArray() 함수를 사용해요. 반대로 객체 타입 배열을 원시 타입 배열로 변환하려면 .toBooleanArray(), .toByteArray(), .toCharArray() 등을 써요.

배열 다루기

배열은 컬렉션과 같은 많은 연산을 지원해요. 반복, 검색, 정렬, 변환을 포함해서요. Kotlin에서는 배열을 함수에 가변 개수의 인자를 전달하는 데 쓰거나 배열 자체에 연산을 수행하는 방식으로 다룰 수 있어요. 가장 흔한 프로퍼티와 함수는 다음 표에서 찾을 수 있어요.

멤버 반환값
size 요소의 개수
indices 유효한 인덱스의 범위
lastIndex 마지막 유효 인덱스
first()last() 첫 번째와 마지막 요소
isEmpty()isNotEmpty() 배열이 비었는지 아닌지에 따라 true
contains() 배열에 요소가 있으면 true

참고: 배열 프로퍼티와 함수에 대한 더 자세한 내용은 API 레퍼런스에서 확인할 수 있어요.

이 절에서는 가장 흔히 쓰는 연산 몇 가지를 소개할게요.

요소 접근과 수정

배열에서 요소에 접근하고 수정하려면 인덱스 접근 연산자 ([])를 사용해요.

fun main() {
    val simpleArray = arrayOf(1, 2, 3)
    val twoDArray = Array(2) { Array<Int>(2) { 0 } }
    // 요소에 접근하고 수정해요
    simpleArray[0] = 10
    twoDArray[0][0] = 2
    // 수정된 요소를 출력해요
    println(simpleArray[0])
    // 10
    println(twoDArray[0][0])
    // 2
}

참고: 배열의 범위를 벗어난 인덱스에 접근하려 하면 Kotlin은 런타임에 ArrayIndexOutOfBoundsException을 던져요.

범위의 요소를 제자리에서 교체하려면 fill(element, fromIndex, toIndex) 함수를 쓸 수도 있어요. fromIndex는 포함되고 toIndex는 제외돼요.

fun main() {
    val arr = IntArray(3)
    println(arr.joinToString())
    // 0, 0, 0
   arr.fill(1)
   println(arr.joinToString())
   // 1, 1, 1
   arr.fill(0, 0, 2)
   println(arr.joinToString())
   // 0, 0, 1
}

Kotlin에서 배열은 불변(invariant)이에요. 즉 Array<String>Array<Any>의 서브타입이 아니라는 뜻이에요. 이는 가능한 런타임 타입 실패를 막아 줘요. 공변성을 표현하려면 Array<out Any> 타입 프로젝션을 사용해요.

fun main() {
    fun printArr(arr: Array<out Any>) {
        for (item in arr) print("$item, ")
    }
    printArr(arrayOf("k", "t", "n"))
    // k, t, n,
}

요소 추가와 제거

배열은 크기가 고정이기 때문에 .add().remove() 함수를 지원하지 않아요. 이런 연산을 하려면 새 배열을 만들어야 해요. 이를 위해 다음 옵션 중 하나를 쓸 수 있어요.

fun main() {
    var arr = intArrayOf(0, 1, 2)
    arr = arr.copyOf(arr.size + 1)
    println(arr.joinToString())
    // 0, 1, 2, 0

    arr[arr.lastIndex] = 3
    println(arr.joinToString())
    // 0, 1, 2, 3
}
  • + 또는 += 연산자 사용:
fun main() {
    var arr = intArrayOf(0, 1, 2)
    arr += 3
    println(arr.joinToString())
    // 0, 1, 2, 3

    arr = arr + intArrayOf(4, 5)
    println(arr.joinToString())
    // 0, 1, 2, 3, 4, 5
}

참고: 요소를 자주 추가하거나 제거해야 한다면 가변 컬렉션을 사용하세요.

배열 비교

두 배열이 같은 요소를 같은 순서로 갖는지 비교하려면 .contentEquals().contentDeepEquals() 함수를 사용해요.

fun main() {
    val simpleArray = arrayOf(1, 2, 3)
    val anotherArray = arrayOf(1, 2, 3)
    // 배열의 내용을 비교해요
    println(simpleArray.contentEquals(anotherArray))
    // true

    // 요소를 바꾼 뒤 중위 표기법으로 배열 내용을 비교해요
    simpleArray[0] = 10
    println(simpleArray contentEquals anotherArray)
    // false
}

참고: 배열의 내용을 비교할 때 동등(==)과 부등(!=) 연산자를 사용하지 마세요. 이 연산자는 할당된 변수가 같은 객체를 가리키는지를 확인해요. Kotlin 배열이 왜 이렇게 동작하는지 더 알고 싶다면 블로그 포스트를 참고하세요.

배열 변환

Kotlin에는 배열을 변환하는 유용한 함수가 많아요. 이 절에서는 그중 일부를 소개해요. 전체 목록은 API 레퍼런스에서 확인할 수 있어요.

합 (Sum)

배열의 모든 요소의 합을 반환하려면 .sum() 함수를 사용해요.

fun main() {
    val sumArray = arrayOf(1, 2, 3)
    println(sumArray.sum())
    // 6
}

참고: .sum() 함수는 Int 같은 숫자 데이터 타입의 배열에서만 사용할 수 있어요.

정렬과 셔플

배열의 요소를 자연 순서대로 정렬하려면 .sort() 함수를, 무작위로 섞으려면 .shuffle() 함수를 사용해요.

fun main() {
    val simpleArray = arrayOf(1, 2, 3)
    // 요소를 무작위로 섞어요
    simpleArray.shuffle()
    println(simpleArray.joinToString())
    // 요소를 정렬해요
    simpleArray.sort()
    println(simpleArray.joinToString())
    // 1, 2, 3
}

원본을 수정하지 않고 정렬된 새 배열을 얻으려면 대신 .sortedArray() 함수를 사용해요.

함수에 가변 개수의 인자 전달

Kotlin에서는 vararg 파라미터로 함수에 가변 개수의 인자를 전달할 수 있어요. 메시지 형식을 만들거나 SQL 쿼리를 만들 때처럼 인자 개수를 미리 모를 때 유용해요.

가변 개수의 인자가 담긴 배열을 함수에 전달하려면 확산 연산자(spread operator, *)를 사용해요. 확산 연산자는 배열의 각 요소를 선택한 함수의 개별 인자로 전달해요.

fun main() {
    val lettersArray = arrayOf("c", "d")
    printAllStrings("a", "b", *lettersArray)
    // abcd
}

fun printAllStrings(vararg strings: String) {
    for (string in strings) {
        print(string)
    }
}

더 자세한 내용은 가변 개수의 인자 (varargs)를 참고하세요.

컬렉션으로 변환하기

어떤 API는 배열을 쓰고 어떤 API는 컬렉션을 쓰는 상황이라면, 배열을 컬렉션으로, 또는 그 반대로 변환할 수 있어요. 이를 위해 .toList(), .toSet(), .toMap() 함수를 사용해요. 이 함수들은 배열의 내용을 독립된 복사본으로 복사해요. 이후 배열에서 발생하는 변경은 반영되지 않아요.

List나 Set으로 변환

배열을 ListSet으로 변환하려면 .toList().toSet() 함수를 사용해요.

fun main() {
    val simpleArray = arrayOf("a", "b", "c", "c")
    // Set으로 변환해요
    println(simpleArray.toSet())
    // [a, b, c]

    // List로 변환해요
    println(simpleArray.toList())
    // [a, b, c, c]
}

원본 배열이 다른 곳에서 변경되지 않거나 공유되지 않는다는 게 완전히 확실하지 않다면 .asList()와 관련 as* 함수를 사용하지 마세요. 이 함수들은 배열을 복사하는 대신 감싸요. 그래서 배열의 변경이 리스트에 반영되고, 그 반대도 마찬가지예요.

fun main() {
    val simpleArray = arrayOf("a", "b", "c")
    val list = simpleArray.asList()
    simpleArray[0] = "d"
    println(list)
    // [d, b, c]
}

Map으로 변환

배열을 Map으로 변환하려면 .toMap() 함수를 사용해요.

Pair<K,V>의 배열만 Map으로 변환할 수 있어요. Pair 인스턴스의 첫 번째 값은 키가 되고, 두 번째 값은 값이 돼요. 같은 키가 두 번 이상 나타나면 마지막 값이 사용돼요.

이 예시는 중위 표기법으로 .to 함수를 호출해서 Pair 튜플을 만들어요.

fun main() {
    val pairArray = arrayOf("apple" to 120, "banana" to 150, "cherry" to 90, "apple" to 140)
    // Map으로 변환해요
    // 과일이 키, 칼로리 숫자가 값이 돼요
    // 마지막 "apple" 값이 첫 번째 값을 덮어써요
    println(pairArray.toMap())
    // {apple=140, banana=150, cherry=90}
}

다음 단계

더 알아보기 (Learn more)

배열은 컬렉션보다 제약이 많은 구조라서, 언제 배열을 써야 하고 언제 컬렉션을 써야 하는지를 기준으로 접근하는 게 좋아요. 원시 타입 배열과 일반 배열의 차이, 그리고 == 대신 contentEquals를 써야 하는 이유를 직접 코드로 확인해 보면 헷갈리는 부분이 자연스럽게 정리돼요.