배열
배열
배열(array)은 같은 타입 또는 그 서브타입의 값을 고정된 개수만큼 담는 데이터 구조예요. 배열의 요소는 순서를 가지고 인덱스로 접근해요. Kotlin은 Array<T> 클래스와 원시 타입 배열을 제공해요.
출처: Kotlin 공식 문서
본문
배열은 언제 쓸까
배열은 Java API와의 상호 운용이나 저수준 요구 사항을 다룰 때 사용해요. 예를 들어 일반 애플리케이션에서 요구하는 수준 이상의 성능 요구가 있거나, 커스텀 데이터 구조를 직접 만들어야 할 때요.
대부분의 사용 사례에서는 배열 대신 컬렉션을 사용하는 걸 권장해요.
| 기능 | 배열(Array) | 컬렉션(Collection) |
|---|---|---|
| 크기 | 고정 | 타입에 따라 다름 |
| 읽기 전용 변형 | 없음, 항상 변경 가능 | 있음 (List와 Set) |
| 요소 추가·제거 | 기본 지원 없음. 새 배열에 할당하고 복사해야 함 | 있음 (가변 컬렉션) |
==로 구조적 동일성 |
아니요, 참조를 비교함. .contentEquals()를 대신 사용 |
예 |
| 원시 값 | 원시 타입 배열은 박싱 없이 값을 저장 | 보통 박싱됨 |
| Java 상호 운용 | T[]에 매핑 |
java.util.List와 java.util.Set에 매핑 |
| 함수형 스타일 필터·변환 | 제한적 | 광범위 |
배열을 컬렉션으로 변환하는 방법도 알아두면 좋아요.
배열 만들기
배열을 만들려면 다음을 사용할 수 있어요.
arrayOf(),arrayOfNulls(),emptyArray()함수Array생성자
참고:
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() 함수를 지원하지 않아요. 이런 연산을 하려면 새 배열을 만들어야 해요. 이를 위해 다음 옵션 중 하나를 쓸 수 있어요.
.copyOf함수 사용:
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으로 변환
배열을 List나 Set으로 변환하려면 .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}
}
다음 단계
- 대부분의 사용 사례에 컬렉션을 권장하는 이유는 컬렉션 개요에서 더 알아볼 수 있어요.
- 다른 기본 타입도 알아보세요.
- Java 개발자라면 Java to Kotlin 컬렉션 마이그레이션 가이드를 읽어보세요.
더 알아보기 (Learn more)
배열은 컬렉션보다 제약이 많은 구조라서, 언제 배열을 써야 하고 언제 컬렉션을 써야 하는지를 기준으로 접근하는 게 좋아요. 원시 타입 배열과 일반 배열의 차이, 그리고 == 대신 contentEquals를 써야 하는 이유를 직접 코드로 확인해 보면 헷갈리는 부분이 자연스럽게 정리돼요.