코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 배열과 컬렉션을 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의에 소금을 조금 친 내용이다
배열(Array)
Kotlin에서 배열은 Array 클래스로 표현되며, 고정된 요소를 지정할 때 사용한다. Java와 달리 Kotlin에서는 배열을 직접 사용하는 경우가 많지 않다. 이는 이펙티브 자바에서도 “배열보다는 리스트를 사용하라”고 권장하는 것과 비슷한 맥락이다
fun main() {
val array = arrayOf(100, 200) // arrayOf()를 사용하여 배열 생성
// 인덱스를 활용한 반복
for (i in array.indices) { // array.indices는 0부터 size-1 까지의 인덱스 범위는 반환
println("${i} ${array[i]}") // 인덱스 값 출력
}
// withIndex()를 활용한 반복
for((idx, value) in array.withIndex()) { // 인덱스와 값을 동시에 얻을 수 있다
println("$idx $value")
}
// 배열에 값 추가 (새로운 배열 반환)
// plus() 함수는 기존 배열에 요소를 추가한 새로운 배열을 반환한다 (원본 배열은 변경되지 않는다)
val newArray = array.plus(300)
}
// Java
public class ArrayEx {
public static void main(String[] args) {
int[] array = {100, 200};
for (int i = 0; i < array.length; i++) {
System.out.printf("%s %s \n", i, array[i]);
}
}
}
컬렉션 (Collection) – List, Set, Map
Kotlin에서는 컬렉션을 다룰 때 가변성(Mutable)을 명시적으로 선언하는 것이 중요하다. 이는 “코드의 의도를 타입으로 표현”하여 컬렉션 수정 가능 여부를 즉시 파악하고 더 안전하며 예측 가능한 코드를 작성할 수 있도록 해준다
가변 컬렉션 vs 불변 컬렉션
- 가변(Mutable) 컬렉션: 요소를 추가, 삭제, 수정할 수 있다 (예: MutableList, MutableSet, MutableMap)
- 불변(Immutable) 컬렉션: 요소를 추가하거나 삭제할 수 없다. 컬렉션 내용은 생성 후 변경되지 않는다 (예: List, Set, Map)
- 주의: 불변 컬렉션이라 하더라도 컬렉션 내에 저장된 참조 타입 객체의 필드는 변경할 수 있다. 예를 들어 List<Money>가 있을 때 list.get(0).price = 5000과 같이 Money 객체 내부의 price 필드를 변경하는 것은 가능하다. 불변 컬렉션은 컬렉션 자체의 구조(요소의 개수, 순서 등)가 변경되지 않는다는 것을 의미한다
List
- 순서가 있는 데이터의 컬렉션으로 중복된 요소를 허용한다
생성
fun main() {
// 불변 리스트 생성: listOf()
// 타입 추론이 가능하여 타입 생략 가능 (예: List<Int>)
val numbers = listOf(100, 200)
// 비어있는 리스트 생성 시에는 타입 명시 필요
val emptyList = emptyList<Int>()
// 함수 매개변수로 전달 시 타입 추론 가능
printNumbers(emptyList()) // printNumbers(numbers: List<Int>) 시그니처로 인해 타입 추론
// 가변 리스트 생성: mutableListOf()
val mutableNumers = mutableListOf(100, 200)
mutableNumers.add(300) // 요소 추가 가능
mutableNumers[0] = 50 // 요소 수정 가능
}
private fun printNumbers(numbers: List<Int>) {
/* 로직... */
}
접근 및 반복
fun main() {
val numbers = listOf(100, 200)
// 하나 가져오기
println(numbers[0]) // 배열처럼 인덱스 접근 가능 (Kotlin 스타일)
println(numbers.get(0)) // get() 함수 사용도 가능 (Java 스타일)
// For Each
for (number in numbers) {
println(number)
}
// 인덱스와 함께 For Each (withIndex() 활용)
for ((idx, value) in numbers.withIndex()) {
println("$idx $value")
}
}
- 일반적으로 불변 리스트를 먼저 고려하고 요소의 추가/삭제/수정이 필요한 경우에만 가변 리스트로 변경하는 것이 보통 좋다. 이는 코드의 안정성을 높이고 잠재적인 버그를 줄이는데 도움이 된다
Set (집합)
- 순서가 없고, 중복된 요소를 허용하지 않는 컬렉션이다
생성 및 반복
fun main() {
// 불변 Set 생성: setOf()
val numbers = setOf(100, 200, 200) // 중복된 200은 하나만 저장된다 (100, 200)
// 가변 Set 생성: mutableSetOf()
val mutableNumbers = mutableSetOf(100, 200)
mutableNumbers.add(300)
// For Each
for (number in numbers) {
println(number)
}
// 인덱스와 함께 For Each (withIndex() 활용))
for ((idx, value) in numbers.withIndex()) {
println("$idx $value")
}
}
- Kotlin의 기본 Set 구현체는 LinkedHashSet으로 요소의 삽입 순서를 유지한다
Map
- 키-값 쌍으로 이루어진 컬렉션이다. 키는 중복될 수 없으며, 각 키는 하나의 값에 매핑한다
생성 및 반복
fun main() {
// 가변 Map 생성: mutableMapOf()
// 키와 값의 타입을 명시해야 한다 (예: <Int, String>)
val oldMap = mutableMapOf<Int, String>()
oldMap.put(1, "Monday") // put() 함수 (Java 스타일)
oldMap[2] = "Tuesday" // 인덱스 접근 방식 (Kotlin 스타일)
oldMap[3] = "Wednesday"
// 불변 Map 생성: mapOf()와 중위 호출 (to 키워드)
val newMap = mapOf(1 to "Monday", 2 to "Tuesday", 3 to "Wednesday")
// 키를 통해 값 접근
println(nweMap[1]) // "Monday"
// 키를 활용한 반복
for (key in newMap.keys) {
println("$key -> ${newMap[key]}")
}
// 키와 값을 동시에 활용한 반복 (entries 활용)
for ((key, value) in newMap.entries) {
println("$key -> $value")
}
// 또는 구조 분해 선언을 사용해여 직접 반복
for ((key, value) in newMap) {
println("$key -> $value")
}
}
컬렉션의 Null 가능성 (Nullability)
- Kotlin에서는 ? 위치에 따라 null 가능성의 의미가 달라지므로 정확히 파악하는 것이 중요하다
List<Int?>
- 리스트 자체는 null이 아니지만, 리스트 요쇼가 null 일 수 있다
val list: List<Int?> = listOf(1, 2, null, 4) println(list.size) // 4 (리스트 자체는 항상 안전) println(list[2]) // null (요소가 null일 수 있음) println(list[0] + 10) // 컴파일 에러: Int? 타입이므로 null 체크 필요 println(list[0]!! + 10) // 11 (null 아님 단언 연산자 사용 또는 안전 호출 / 엘비스 연산자 활용 권장)
List<Int>?
- 리스트의 요소는 null이 될 수 없지만, 리스트 자체가 null 일 수 있다
val list: List<Int>? = null // 또는 listOf(1, 2, 3)
println(list?.size) // null (리스트가 null일 수 있으므로 안전 호출)
list?.let {
println(it[0] + 10) // 'it'은 list<Int> 타입이므로 요소는 확실히 non-null
}
List<Int?>?
- 리스트 자체도 null 일 수도 있고 리스트 요소도 null 일 수 있다
val list: List<Int?>? = null // 또는 listOf (1, null, 3)
list?.let { safeList -> // 리스트가 null이 아닌 경우
safeList.forEach { element -> // 요소는 Int? 타입
element?.let { value -> // 요소가 null이 아닌 경우
println(value + 10)
}
}
실용적 사용 예시 – 함수 파라미터
// 요소만 nullable: 빈 값들을 처리할 때
fun proccessUserNames(name: List<String?>) {
names.filterNotNull().forEach { name -> // null이 아닌 이름만 필터링
println("Hello, $name")
}
}
// 리스트만 nullable: 선택적 데이터를 받을 때
fun proccessOptionalData(data: List<String>?) {
data.forEach { item -> // item은 확실히 String 타입
println(item.uppercase())
}
}
실용적 사용 예시 – 반환 타입
// 빈 트스트 vs null로 의미 구분
fun findUsers(query: String): List<User>? {
return if (query.isBlank()) null // 검색어가 비어있으면 null 반환 (검색을 수행하지 않음)
else userRepository.search(query) // 빈 리스트 또느 결과 리스트 반환
}
- 이처럼 ?의 위치가 null 가능성의 범위를 결정하므로, 이를 이해하여 정확한 타입 설계와 안전한 null 처리를 할 수 있다
출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)