컬렉션은 엘리먼트들로 이뤄진 그룹일 저장하기 위해 설계된 객체다.
컬렉션을 조작하는 모든 연산은 인라인 함수다. 따라서 이런 연산을 사용해도 함수 호출이나 람다 호출에 따른 부가 비용이 들지 않는다.
코틀린 컬렉션 타입은 기본적으로 네 가지로 분류할 수 있다.
이터러블은 Iterable< T > 타입으로 표현되며, 일반적으로 즉시 계산되는 상태가 있는 컬렉션을 표현한다.
상태가 있다는 말은 컬렉션이 원소를 필요할 때 생성하는 제너레이터 함수를 유지하지 않고 원소를 저장한다는 뜻이다. 즉시 계산이라는 원소가 필요 시점이 아닌 최초 생성시 초기화된다는 뜻이다.
val list = listOf("red", "orange", "yellow")
for (item in list) {
println(item + "")
}
코틀린 Iterator 타입은 자바와 동일하다.
다른 점은 코틀린은 불변 컬렉션과 가변 컬렉션을 구분한다는 점이다. 기본이 불변이고 가변인 경우 MutableIterator를 사용해야 한다.
ㅤval list = ArrayList<String>() list.add("abc") list = ArrayList<String>() // error: val cannot be reassigned
ㅤㅤ
불변 컬렉션 타입에서 유용한 특징으로는 공변성이 있다. 공변성이라는 말은 T가 U의 하위 타입인 경우 Iterator< T >도 Iterator< U >의 하위 타입이라는 뜻이다.fun processCollection(c: Iterable<Any>) { ... } ㅤ fun main() { val list = listOf("a", "b", "c") // List<String> processCollection(list) // 공변성으로 List<String>을 List<Any>로 전달 가능 }
ㅤ
하지만 가변 컬렉션의 경우 이런 코드가 작동하지 않는다.
컬렉션은 이터러블의 하위 분류이고, 리스트와 집합은 컬렉션을 상속받은 클래스이다.
리스트는 인덱스를 통한 원소 접근이 가능한, 순서가 정해진 원소의 컬렉션이다.
집합은 유일한 원소들로 이뤄진 컬렉션이다.
이터러블과 비슷하게 시퀀스도 iterator() 메서드를 제공한다.
하지만 시퀀스는 지연 계산을 가정하기 때문에 iterator()의 의도가 이터러블과 다르다. 대부분의 시퀀스 구현은 객체 초기화 시 원소를 초기화하지 않고 요청에 따라 원소를 계산한다. 대부분의 시퀀스 구현은 상태가 없다. 즉, 지연 계산한 컬렉션 원소 중에 정해진 개수의 원소만 저장한다는 뜻이다. 반면 이터러블은 원소 개수에 비례해 메모리를 사용한다.
자바의 스트림과 시퀀스가 유사하다.
맵은 키와 값 쌍으로 이뤄진 집합이다. 키는 유일해야 한다. 맵 자체는 컬렉션의 하위 타입이 아니지만 컬렉션처럼 사용은 가능하다.
자바처럼 코틀린도 Comparable<>과 Comparator<> 타입을 지원하며, 몇몇 컬렉션 연산에 이를 활용한다.
비교 가능 연산자 Comparable 인스턴스는 자연적인 순서를 지원하며, 이런 타입의 인스턴스들은 모두 동일한 타입의 다른 인스턴스와 순서를 비교할 때 쓸 수 있는 compareTo() 메서드를 포함한다.
class Person(
val firstName: String,
val familyName: String,
val age: Int
) : Comparable<Person> {
val fullName get() = "$firstName $familyName"
override fun compareTo(other: Person) = fullName.compareTo(other.fullName)
}
fun main() {
val a = Person("a", "pple", 29)
val b = Person("b", "anana", 30)
val b2 = Person("b", "anana", 30)
val c = Person("c", "anary", 25)
println("a.compareTo(b): ${a.compareTo(b)}") // a.compareTo(b): -1
println("c.compareTo(b): ${c.compareTo(b)}") // c.compareTo(b): 1
println("b2.compareTo(b): ${b2.compareTo(b)}") // b2.compareTo(b): 0
}
compareTo() 함수는 자바와 똑같다. 즉, 현재(수신 객체) 인스턴스가 인자로 받은 상대 인스턴스보다 더 크면 양수, 더 작으면 음수, 같으면 0을 반환한다.
A.compareTo(B)
- A > B : 양수
- A == B : 0
- A < B : 음수
어떤 클래스를 여러 가지 방법으로 비교해야 하는 경우도 많다. 예를 들어 Person의 컬렉션을 성, 이름, 나이 중 하나를 기준으로 정렬하거나, 여러 프로퍼티의 조합을 기준으로 정렬할 수도 있다. 이런 경우 비교기 Comparator 클래스를 사용한다. 자바와 마찬가지로 Comparator< T > 클래스는 타입 T의 인스턴스 객체를 두 개 인자로 받는 compare() 함수를 제공한다
class Person(
val firstName: String,
val familyName: String,
val age: Int
) : Comparable<Person> {
val fullName get() = "$firstName $familyName"
override fun compareTo(other: Person) = fullName.compareTo(other.fullName)
}
fun main() {
val a = Person("a", "pple", 29)
val b = Person("b", "anana", 30)
val b2 = Person("b", "anana", 30)
val c = Person("c", "anary", 25)
val AGE_COMPARATOR = Comparator<Person>{ p1, p2 ->
p1.age.compareTo(p2.age)
}
println("compare(a, b) : ${AGE_COMPARATOR.compare(a, b)}") // compare(a, b) : -1
println("compare(a, c) : ${AGE_COMPARATOR.compare(a, c)}") // compare(a, c) : 1
println("compare(b, b2) : ${AGE_COMPARATOR.compare(b, b2)}") // compare(b, b2) : 0
val AGE_COMPARATOR2 = compareBy<Person>{ it.age }
println("compare(a, b) : ${AGE_COMPARATOR2.compare(a, b)}") // compare(a, b) : -1
println("compare(a, c) : ${AGE_COMPARATOR2.compare(a, c)}") // compare(a, c) : 1
println("compare(b, b2) : ${AGE_COMPARATOR2.compare(b, b2)}") // compare(b, b2) : 0
val REVERSE_AGE_COMPARATOR = compareByDescending<Person>{ it.age }
println("compare(a, b) : ${REVERSE_AGE_COMPARATOR.compare(a, b)}") // compare(a, b) : 1
println("compare(a, c) : ${REVERSE_AGE_COMPARATOR.compare(a, c)}") // compare(a, c) : -1
println("compare(b, b2) : ${REVERSE_AGE_COMPARATOR.compare(b, b2)}") // compare(b, b2) : 0
}
fun main() {
val list = ArrayList<String>()
list.add("red")
list.add("blue")
list.add("green")
list.remove("green")
println(list) // [red, blue]
}
fun main() {
val set = HashSet<Int>()
set.add(12)
set.add(21)
set.add(12)
println(set) // [12, 21]
}
val map = TreeMap<Int, String>()
map[20] = "twenty"
map[10] = "ten"
println(map) // {10=tem, 20=twenty}
반복
val map = mapOf(1 to "one", 2 to "two", 3 to "three")
for ((key, value) in map) {
...
}
intArrayOf(1, 2, 3).forEach { println(it*it) }
listOf("a", "b", "c").forEach { println("'$it'") }
sequenceOf("a", "b", "c").forEach { println("'$it'") }
mapOf(1 to "one", 2 to "two", 3 to "three").forEach { (key, value) ->
println("$key -> $value")
}
listOf(1, 2, 3).forEachIndexed { i, n -> println("$i: $n") }
기본 기능
fun main() {
val list = listOf(10, 20, 30)
println(list.size) // 3
println(list.isEmpty()) // false
println(list.contains(20)) // true
// contains는 in으로 변경 가능
println(20 in list) // true
println(list.containsAll(listOf(10, 20, 30))) // true
}
조회
fun main() {
val list = listOf(1, 4, 6, 2, 4, 1, 7)
println(list.get(1)) // 4
println(list[1]) // 4
println(list.indexOf(2)) // 3, list에서 2가 위치한 index 반환
println(list.indexOf(3)) // -1, list에 3이 없어서 -1 반환
println(list.lastIndexOf(1)) // 5, list에서 마지막 1의 index 반환
}
get() 메서드 호출보다 인덱스 사용을 권장한다.
복사
fun main() {
val mutableList = arrayListOf<Int>(1, 4, 6, 2, 4, 1, 7)
val subList = mutableList.subList(2, 6) // 시작 index 포함, 끝 index 제외
println(subList) // [6, 2, 4, 1]
subList.set(0, 9) // 뷰의 데이터 변경 시 원본에도 영향
println(mutableList) // [1, 4, 9, 2, 4, 1, 7]: 6 -> 9 변경 됨
println(subList) // [9, 2, 4, 1]
}
subList() 함수는 시작 인덱스(포함)와 끝 인덱스(제외)로 지정한 리스트의 일부분에 대한 래퍼를 만든다. 이 뷰는 원본 컬렉션의 데이터를 공유하기 때문에 가변 리스트의 경우 뷰와 원본의 변화가 서로 반영된다.
집합 set의 경우 Collection에 있는 공통 메서드만 지원한다.
val map = mapOf(1 to "I", 5 to "V", 10 to "X", 50 to "L")
println(map.isEmpty()) // false
println(map.size) // 4
println(map.get(5)) // V
println(map[10]) // X
println(map[1000]) // null
println(map.getOrDefault(100, "?")) // ?
println(map.getOrElse(100) { "?" }) // ?
println(map.containsKey(10)) // true
println(map.containsValue("C")) // false
println(map.keys) // [1, 5, 10, 50]
println(map.values) // [I, V, X, L]
println(map.entries) // [1=I, 5=V, 10=X, 50=L]
println(listOf(1, 2, 3).first()) // 1
println(listOf(1, 2, 3).last()) // 3
println(emptyArray<String>().first()) // Exception
println(emptyArray<String>().firstOrNull()) // null
println(listOf(1, 2, 3).first { it > 2 }) // 3
println(listOf(1, 2, 3).lastOrNull { it < 0}) // null
println(listOf(1).single())
println(listOf(1, 2).single()) // List has more than one element.
println(emptyArray<String>().single()) // Array is empty.
println(listOf(1, 2).singleOrNull()) // null
println(emptyArray<String>().singleOrNull()) // null
println(listOf(1, 2, 3).elementAt(2)) // 3
println(sortedSetOf(1, 2, 3).elementAtOrNull(-1)) // null
println(sortedSetOf(1, 2, 3).elementAtOrNull(2)) // 3
println(sortedSetOf(1, 2, 3).elementAtOrElse(1) { "???" }) // 2
println(sortedSetOf(1, 2, 3).elementAtOrElse(5) { "???" }) // ???
fun main() {
println(listOf(1, 2, 3).all { it < 10 }) // true
println(listOf(1, 2, 3, 10).all { it < 10 }) // false
println(listOf(1, 2, 3).none { it > 10 }) // true
println(listOf(1, 2, 3).any { it < 0 }) // false
println(listOf(1, 2, 3).any { it < 2 }) // true
}
빈 컬렉션의 경우 all()과 none() 함수는 true를 반환하고, any() 함수는 false를 반환한다.
println(listOf(1, 2, 3).count()) // 3
println(listOf(1, 2, 3).count { it > 2 }) // 1
println(listOf(1, 2, 3).sum()) // 6
println(listOf(1, 2, 3).sumOf { it*2 }) // 12 : (1*2) + (2*2) + (3*2)
println(listOf(1, 2, 3).average()) // 2.0
println(listOf(1, 2, 3).minOrNull()) // 1
println(listOf(1, 2, 3).maxOrNull()) // 3
min(), max()는 코틀린 1.4부터 사용 금지 처리됐다.
class Person(
val name: String,
val age: Int
) {
override fun toString() = "$name: $age"
}
fun main() {
val persons = sequenceOf(
Person("jake", 29),
Person("dawn", 30),
Person("lobo", 35),
Person("zelen", 26),
Person("ace", 46)
)
println(persons.minByOrNull { it.name }) // ace: 46
println(persons.maxByOrNull { it.name }) // zelen: 26
// minWithOrNull, maxWithOrNull 함수는 비교기 Comparator를 받는다.
val NAME_COMPARATOR = Comparator<Person>{ p1, p2 ->
p1.name.compareTo(p2.name)
}
println(persons.minWithOrNull(NAME_COMPARATOR)) // ace: 46
println(persons.maxWithOrNull(NAME_COMPARATOR)) // zelen: 26
}
fun main() {
println(listOf(1, 2, 3)) // [1, 2, 3]
println(listOf(1, 2, 3).joinToString(prefix = "{", postfix = "}")) // {1, 2, 3}
println(listOf(1, 2, 3).joinToString(separator = "|")) // 1|2|3
val builder = StringBuilder("joinTo: ")
println(listOf(1, 2, 3).joinTo(builder, separator = "|")) // joinTo: 1|2|3
}
fun main() {
println(listOf(1, 2, 3, 4).reduce { acc, n -> acc * n }) // 24
println(listOf(1, 2, 3, 4).reduce { acc, n -> acc + n }) // 10
println(listOf("a", "b", "c", "d").reduce { acc, n -> acc + n }) // abcd
println(listOf(1, 2, 3, 4).reduceIndexed { i, acc, n -> if (i % 2 == 1) acc + n else acc }) // 7: 1 + 2 + 4 (홀수만 처리하고 싶었지만 첫 번째 원소도 포함되어 처리 됨)
println(listOf(1, 2, 3, 4).foldIndexed(10) { i, acc, n -> if (i % 2 == 1) acc + n else acc }) // 16: 2 + 4 + 10(초깃값)
}
fun main() {
val list = listOf<String>("red", "blue", "green", "black", "green")
println(list.filter { it.length > 3 }) // [blue, green, black, green]
println(list.filterNot { it.length > 3 }) // [red]
val map = mapOf<String, Int>("I" to 1, "V" to 5, "X" to 10, "L" to 50)
println(map.filter { it.value > 5 }) // {X=10, L=50}
println(map.filterKeys { it == "X" }) // {X=10}
println(map.filterValues { it > 5 }) // {X=10, L=50}
}
fun main() {
println(setOf("red", "green", "blue").map { it.length }) // [3, 5, 4]
println(listOf(1, 2, 3, 4).map { it * it }) // [1, 4, 9, 16]
println(setOf("red", "green", "blue").flatMap {it.asIterable() }) // [r, e, d, g, r, e, e, n, b, l, u, e]
}
fun main() {
println(intArrayOf(1, 4, 9, 10, 5, 2, 3).sorted()) // [1, 2, 3, 4, 5, 9, 10]
println(intArrayOf(1, 4, 9, 10, 5, 2, 3).sortedDescending()) // [10, 9, 5, 4, 3, 2, 1]
println(intArrayOf(1, 4, 9, 10, 5, 2, 3).sorted().reversed()) // [10, 9, 5, 4, 3, 2, 1]
}
fun main() {
val persons = listOf(
Person("jake", 29),
Person("dawn", 30),
Person("lobo", 35),
Person("zelen", 26),
Person("ace", 46)
)
println(persons.sortedWith(AGE_COMPARATOR)) // [zelen: 26, jake: 29, dawn: 30, lobo: 35, ace: 46]
println(persons.sortedWith(AGE_COMPARATOR).reversed()) // [ace: 46, lobo: 35, dawn: 30, jake: 29, zelen: 26]
println(persons.sortedBy { it.age }) // [zelen: 26, jake: 29, dawn: 30, lobo: 35, ace: 46]
println(persons.sortedByDescending { it.age }) // [ace: 46, lobo: 35, dawn: 30, jake: 29, zelen: 26]
}