[Kotlin in Action] 6장. 컬렉션과 시퀀스

도서 <코틀린 인 액션 2/e>를 읽으며 코틀린에 대해 이해한 내용을 정리한 글이다.
6장의 학습 목표는 코틀린으로 컬렉션을 우아하게, 간결하게 다루자! 이다. 컬렉션과 관련된 API들을 소개하는 장이라 필요할 때 이들을 떠올릴 수 있게 인지하고 있으면 좋을 듯하다. 자바의 스트림과 유사한 시퀀스에 대해 이해하고, 컬렉션에서 보다 효율적인 연산을 수행하는 방법을 배운다.
6.1 컬렉션에 대한 함수형 API
컬렉션을 다룰 때 유용하게 쓸 수 있는 함수형 API들은 다음과 같다.
filter, map
기본적으로 출력 컬렉션을 조작한다. 입력 컬렉션의 원소들은 그대로이다
people.filter { it.age >= 30 }
- 이름 그대로 조건에 맞는 원소들을 필터링해 출력 컬렉션을 반환한다.
numbers.map(it * it)
- 입력 컬렉션의 원소를 지정된 함수로 변환해 출력 컬렉션을 반환한다.
reduce, fold
컬렉션의 정보를 합치는 데 사용한다.
list.reduce { acc, element ->
acc + element
}
- 컬렉션의 첫 번째 값을 누적기에 넣고 호출마다 다음 인자가 전달된다.
fold는 시작 값을 선택할 수 있고, 첫 번째 원소부터 누적 값으로 전달된다.
all, any, none, count, find
조건을 만족하는지 판단한다.
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// all: 모든 원소가 짝수인가?
println(numbers.all { it % 2 == 0 })
// any: 하나라도 짝수인가?
println(numbers.any { it % 2 == 0 })
// none: 짝수가 하나도 없는가?
println(numbers.none { it % 2 == 0 })
// count: 짝수의 개수
println(numbers.count { it % 2 == 0 })
// find: 조건을 만족하는 첫 번째 짝수
println(numbers.find { it % 2 == 0 })
// false
// true
// false
// 5
// 2
all, any, none은 조건을 만족하는지 판단한다.count는 조건을 만족하는 원소의 개수를,find는 조건을 만족하는 첫 번째 원소를 반환한다.
partition
조건에 따라 리스트를 분할해 리스트의 쌍(Pair)으로 만든다.
val (even, odd) = listOf(1, 2, 3, 4, 5).partition { it % 2 == 0 }
// Pair를 반환
- 리스트를 2개로 분할한다. (조건 참, 조건 거짓)
groupBy
리스트를 여러 그룹으로 이루어진 맵(Map)으로 바꾼다.
val grouped = listOf("apple", "banana", "avocado").groupBy { it.first() }
println(grouped) // {a = [apple, avocado], b = [banana]}
associate, associateWith, associateBy
컬렉션을 맵으로 변환한다.
val assoc = listOf("a", "bb", "ccc").associate { it to it.length }
println(assoc) // {a=1, bb=2, ccc=3}
associate는 원소를 키와 값으로 직접 지정한다.
val assocWith = listOf("a", "bb", "ccc").associateWith { it.length }
println(assocWith) // {a=1, bb=2, ccc=3}
associateWith는 원소를 키로, 람다 결과를 값으로 매핑한다.
val assocBy = listOf("a", "bb", "ccc").associateBy { it.length }
println(assocBy) // {1=a, 2=bb, 3=ccc}
associateBy는 람다 결과를 키로, 원소를 값으로 매핑한다.
replaceAll, fill
가변 컬렉션의 원소를 변경한다.
val names = mutableListOf("Martin", "Samuel")
names.replaceAll { it.uppercase() }
names.fill("(redacted)")
replaceAll은 각 원소를 람다의 결과를 사용해 대체한다.fill은 컬렉션의 모든 원소를 지정된 값으로 덮어쓴다.
ifEmpty
컬렉션 입력이 비어있지 않는 경우에만 처리하고 싶을 때 사용한다. 다른 언어들의 isEmpty랑 이름이 다르다
val empty = emptyList<String>()
println(empty.ifEmpty { listOf("no", "values", "here" )})
- 컬렉션이 비어있을 때 기본값을 생성하는 람다를 제공
- cf. 문자열에서는 ifBlank를 사용
chunked, windowed
연속적인 데이터 값들에서 유용하게 쓸 수 있다.
val parts = (1..10).chunked(3)
println(parts) // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
- 주어진 크기의 서로 겹치지 않는 부분으로 나누기
val windows = (1..5).windowed(3)
println(windows) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
- 슬라이딩 윈도우 사용하기
zip
두 개의 컬렉션을 합친다.
val zipped = listOf("a", "b", "c").zip(listOf(1, 2, 3))
println(zipped) // [(a, 1), (b, 2), (c, 3)]
- 반환 값은
List<Pair<T, R>>
flatMap, flatten
내포된 컬렉션의 원소를 처리한다.
val lilbrary = listOf(
Book("A", listOf("man1", "man2", "man3")),
Book("B", listOf("man12", "man23", "man32")),
)
fun main() {
val authors = library.flatMap { it.authors }
// List<List<String>>이 아닌 List<String>이 나온다.
}
flatmap()함수는 다음과 같이 컬렉션 내 컬렉션을 평평한 리스트로 반환한다.- 변환할 것이 없고 컬렉션의 컬렉션을 평평하게 만들 때
flatten()을 사용한다.
6.2 시퀀스
지연 계산 컬렉션 연산
자바 8의 스트림과 비슷하게 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산을 연쇄하는 방법이다.
people.map(Person::name).filter { it.startsWith("A") }
리스트를 반환하는 연산이 연쇄되어 있다. 이 연쇄 호출은 리스트를 2개 만든다. 이정도로는 괜찮겠지만 원소가 수백만 개가 되면 매우 효율이 떨어진다.
코틀린에서는 각 연산이 컬렉션을 직접 사용하는 대신, 시퀀스를 사용하게 만든다.
people
.asSequence()
.map(Person::name)
.filter { it.startsWith("A") }
.toList()
asSequence확장 함수를 통해 어떤 컬렉션이든 시퀀스로 바꿀 수 있다.- 내부적으로
Iterator를 기반으로 동작하며, 매번 새로운 저장 구조를 만들지 않는다. - 최종 연산인
toList()에 와서야 실제 계산이 시작되는, 지연 계산이다. - 모든 연산은 각 원소에 대해 순차적으로 적용된다.
- 각 원소마다 map → filter를 거쳐 최종 결과로 바로 전달된다.
궁금한 점. 정렬같은 경우는 이터레이션만으로 처리가 안 될 것 같은데?
찾아보니 순회로 원소를 모으고, 중간 컬렉션을 생성하여 정렬을 수행한 뒤 다시 시퀀스로 반환한다고 한다. 정렬하고 다시 뭔가를 할 게 아니면 굳이 시퀀스에서 정렬할 필요가 없을 듯
- 컬렉션에 대해 수행하는 연산의 순서도 성능에 영향을 끼친다. 더 빨리 원소를 제거하면 할수록 코드 성능이 좋아진다.
시퀀스 만들기
import java.io.File
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any { it.isHidden }
fun main() {
val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory()) // true
}
- 일반적으로 객체의 조상들로 이루어진 시퀀스를 만들고, 조상의 시퀀스에 대해 어떤 특성을 알아내는 식으로 사용한다.