컬렉션 생성
컬렉션 생성
컬렉션을 만드는 방법은 여러 가지예요. 가장 흔한 표준 라이브러리 함수부터 빌더 함수, 복사, 그리고 다른 컬렉션에서 파생시키는 방법까지. 각각이 언제 어울리는지 함께 살펴볼게요.
출처: Kotlin 공식 문서
본문
요소로부터 생성
컬렉션을 만드는 가장 흔한 방법은 표준 라이브러리 함수 listOf<T>(), setOf<T>(), mutableListOf<T>(), mutableSetOf<T>()를 사용하는 거예요. 컬렉션 요소를 쉼표로 구분한 목록을 인자로 제공하면 컴파일러가 요소 타입을 자동으로 감지해요. 빈 컬렉션을 만들 때는 타입을 명시적으로 지정해야 해요.
val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()
맵에도 같은 방식이 적용되는데, mapOf()와 mutableMapOf() 함수를 사용해요. 맵의 키와 값은 Pair 객체로 전달되는데, 보통 to 중위(infix) 함수로 만들죠.
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
to 표기는 짧게 유지되는 Pair 객체를 만들기 때문에, 성능이 중요하지 않을 때만 사용하는 게 권장돼요. 지나친 메모리 사용을 피하려면 다른 방법을 써야 해요. 예를 들어 mutable 맵을 만들어 쓰기 연산(write operations)으로 채우면 되죠. 이때 apply() 함수가 초기화를 유연하게 유지하는 데 도움을 줘요.
val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }
컬렉션 빌더 함수로 생성
컬렉션을 만드는 또 다른 방법은 빌더 함수인 buildList(), buildSet(), buildMap()을 호출하는 거예요. 이 함수들은 해당 타입의 새 mutable 컬렉션을 만들고, 쓰기 연산으로 채운 뒤, 같은 요소를 가진 읽기 전용 컬렉션을 반환해요.
val map = buildMap { // this는 MutableMap<String, Int>, 키와 값의 타입은 아래 put() 호출에서 추론돼요
put("a", 1)
put("b", 0)
put("c", 4)
}
println(map) // {a=1, b=0, c=4}
빈 컬렉션
요소가 없는 컬렉션을 만드는 함수도 있어요. 바로 emptyList(), emptySet(), emptyMap()이에요. 빈 컬렉션을 만들 때는 컬렉션이 담을 요소의 타입을 지정해야 해요.
val empty = emptyList<String>()
리스트 초기화 함수
리스트에는 리스트 크기와 인덱스에 따라 요소 값을 정의하는 초기화 함수를 받는 생성자 같은 함수가 있어요.
fun main() {
//sampleStart
val doubled = List(3, { it * 2 }) // 나중에 내용을 바꾸고 싶으면 MutableList로
println(doubled)
//sampleEnd
}
구체 타입 생성자
ArrayList나 LinkedList 같은 구체 타입 컬렉션을 만들려면 해당 타입의 생성자를 사용할 수 있어요. Set과 Map의 구현에도 비슷한 생성자가 있어요.
val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val presizedSet = HashSet<Int>(32)
복사
기존 컬렉션과 같은 요소를 가진 컬렉션을 만들려면 복사 함수를 사용할 수 있어요. 표준 라이브러리의 컬렉션 복사 함수는 같은 요소에 대한 참조를 가진 얕은 복사(shallow copy) 컬렉션을 만들어요. 그래서 컬렉션 요소에 대한 변경은 모든 복사본에 반영돼요.
toList(), toMutableList(), toSet() 같은 컬렉션 복사 함수는 특정 시점의 컬렉션 스냅샷을 만들어요. 그 결과는 같은 요소를 가진 새 컬렉션이죠. 원본 컬렉션에서 요소를 추가하거나 제거해도 복사본에는 영향이 없어요. 복사본은 원본과 독립적으로 변경될 수도 있어요.
class Person(var name: String)
fun main() {
//sampleStart
val alice = Person("Alice")
val sourceList = mutableListOf(alice, Person("Bob"))
val copyList = sourceList.toList()
sourceList.add(Person("Charles"))
alice.name = "Alicia"
println("First item's name is: ${sourceList[0].name} in source and ${copyList[0].name} in copy")
println("List size is: ${sourceList.size} in source and ${copyList.size} in copy")
//sampleEnd
}
이 함수들은 컬렉션을 다른 타입으로 변환하는 데도 사용할 수 있어요. 예를 들어 리스트에서 집합을 만들거나 그 반대도 가능하죠.
fun main() {
//sampleStart
val sourceList = mutableListOf(1, 2, 3)
val copySet = sourceList.toMutableSet()
copySet.add(3)
copySet.add(4)
println(copySet)
//sampleEnd
}
또는 같은 컬렉션 인스턴스에 대한 새 참조를 만들 수도 있어요. 기존 컬렉션으로 컬렉션 변수를 초기화하면 새 참조가 만들어지죠. 컬렉션 인스턴스가 어떤 참조를 통해 변경되면 그 변경이 모든 참조에 반영돼요.
fun main() {
//sampleStart
val sourceList = mutableListOf(1, 2, 3)
val referenceList = sourceList
referenceList.add(4)
println("Source size: ${sourceList.size}")
//sampleEnd
}
컬렉션 초기화는 가변성을 제한하는 데 사용될 수도 있어요. 예를 들어 MutableList에 대한 List 참조를 만들면, 이 참조를 통해 컬렉션을 수정하려고 할 때 컴파일러가 오류를 발생시켜요.
fun main() {
//sampleStart
val sourceList = mutableListOf(1, 2, 3)
val referenceList: List<Int> = sourceList
//referenceList.add(4) //compilation error
sourceList.add(4)
println(referenceList) // shows the current state of sourceList
//sampleEnd
}
다른 컬렉션에서 함수 호출
컬렉션은 다른 컬렉션에 대한 여러 연산의 결과로 만들어질 수 있어요. 예를 들어 리스트를 필터링하면 필터와 일치하는 요소로 이루어진 새 리스트가 생기죠.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)
//sampleEnd
}
매핑은 변환 결과로부터 리스트를 만들어 내요.
fun main() {
//sampleStart
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })
//sampleEnd
}
연관(Association)은 맵을 만들어 내요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })
//sampleEnd
}