Kotlin의 Collection 함수들
오늘은 Kotlin의 Collection 함수들에 대해서 파헤쳐보는 시간을 가지도록 하겠습니다 ㅎㅎ
현업에서 코틀린을 처음 접한지 벌써 저도 5개월차가 되어가는데요
현업에 다양한 비즈니스 로직을 구현하다보면 Collection에 있는 기본 Util 함수들을 자주 사용하게 됩니다
하지만, 아주 기본적이면서도 구체적으로 동작하는 원리를 알아야 어떻게 활용되는지 알 수 있기 때문에
조금 더 deep 하게 파헤쳐보는 시간을 가져볼까 합니다
참고할 코드
우리가 참고하면서 파헤쳐 보는 코틀린 파일은 단 1개입니다
kotlin.collections.CollectionsKt.class
엥? 너무 짧지 않냐구요?
아닙니다 ㅎㅎ 엄청 길어요 ㅠㅠ
분류별로 하나씩 들어가봅시다~!
여담이지만, 우리는 Sequence에 대한 언급은 여기서 하지 않겠습니다.
Collection은 즉시 N개 단위로 컬렉션을 만들어내는 반면
Sequence는 iterator 기반으로 하나씩 처리하기 때문이죠.
Mapping
흔히 컨버팅과 관련된 계열의 함수입니다.
보통 컨버팅은 2가지 분류의 성격으로 나뉘어지게 되는데요.
1. 1:1 매핑
2. 1:n 매핑
1:1 Mapping에는 우리가 제일 잘 쓰이는 map이 있습니다
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
기본적으로 ArrayList의 Collection을 만들게 됩니다.
혹시 collectionSizeOrDefault 가 보이시나요?
맞습니다.
Collection 인터페이스면 기존에 존재하던 Collection size로 지정되거나 Collection 이 아니면 강제로 10을 세팅하게 됩니다.
1:n Mapping의 대표적인 예는 바로 flatMap 입니다
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
return flatMapTo(ArrayList<R>(), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
for (element in this) {
val list = transform(element)
destination.addAll(list)
}
return destination
}
flatMap은 map과는 다르게 size의 정해져 있지 않습니다.
왜냐하면, 1개의 collection 원소가 몇 개의 collection 원소로 쪼개질지는 컴파일 타임에 모르기 때문이죠
그래서, flatMap은 다소 비싼 연산이라고 할 수 있습니다. 매번매번 size를 늘려가면서 원소를 추가하게 되기 때문이에요
1:n Mapping에는 grouping도 포함되어 있습니다. 바로 groupBy 입니다
public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}
public inline fun <T, K, M : MutableMap<in K, MutableList<T>>> Iterable<T>.groupByTo(destination: M, keySelector: (T) -> K): M {
for (element in this) {
val key = keySelector(element)
val list = destination.getOrPut(key) { ArrayList<T>() }
list.add(element)
}
return destination
}
groupBy는 LinkedHashMap으로 Mapping 하게 됩니다.
어떤 기준(keySelector) 에 의해서 그룹핑을 할 것인지 결정하게 되는 것이죠.
참고로, LinkedHashMap이라서 key 가 생성되는 순서에 따라 순서가 정해지게 됩니다
예를 들어 (key2, key1, key2, key3) -> (key2, key1, key3) 순서로 유지가 되게 되죠
마찬가지로, size가 정해져있지 않습니다 ( MutableList<T>() )
어떤 정도의 크기로 그룹핑될지는 모르기 때문이죠
groupBy가 List로 그룹핑한다면, 1:1 mapping의 성격으로, 원소 자체를 identity를 가지는 associateBy 도 있습니다
public inline fun <T, K> Iterable<T>.associateBy(keySelector: (T) -> K): Map<K, T> {
val capacity = mapCapacity(collectionSizeOrDefault(10)).coerceAtLeast(16)
return associateByTo(LinkedHashMap<K, T>(capacity), keySelector)
}
public inline fun <T, K, M : MutableMap<in K, in T>> Iterable<T>.associateByTo(destination: M, keySelector: (T) -> K): M {
for (element in this) {
destination.put(keySelector(element), element)
}
return destination
}
해당 메서드는 id 값으로 엔티티를 구별하고, 쉽게 찾아오고 싶을 때(?) 자주 사용하게 됩니다
매번 list로 순회하면서 검색하는 것보단 hashMap 으로 바로 꺼내오는 것이 저렴하기 때문이죠
마찬가지로 LinkedHashMap 기반의 순서가 유지되는 성격을 가지고 있습니다
Filtering
이제 collection에서 원하는 것만 찾아오고 싶을 때
우리는 보통 필터링해서 가져온다 라고 쓰죠.
Filtering은 N 개의 Size 에서 0 ~ N 개의 Size 까지 필터링한다고 생각하시면 될 것 같습니다
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
그렇기 때문에 size를 알 수 없습니다. 몇 개가 남을지 모르기 때문이죠
해당하는 predicate을 만족하는 원소들만 List로 만들게 됩니다
다음은 비교적 단순한 first / firstOrNull 입니다
public fun <T> List<T>.firstOrNull(): T? {
return if (isEmpty()) null else this[0]
}
public inline fun <T> Iterable<T>.first(predicate: (T) -> Boolean): T {
for (element in this) if (predicate(element)) return element
throw NoSuchElementException("Collection contains no element matching the predicate.")
}
first는 Nullable한 collection 원소 T 를 반환하게 되고, ( 없으면 예외 )
firstOrNull은 존재하면 해당하는 원소를, 존재하지 않으면 Null을 리턴하게 됩니다
그 외로 index 를 직접 접근하여 가져오는 getOrNull 등 도 있으니 한번 참고하시면 좋을 것 같아요
단순해서 생략하려고 합니다 ㅎㅎ
조건을 전체 또는 부분 만족
collection에 대하여 어떤 특징을 만족하는지 물어보고 싶을 때가 있습니다
그럴 때 자주 사용하는 것이 바로 any / all 이 겠죠
public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return true
for (element in this) if (!predicate(element)) return false
return true
}
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return false
for (element in this) if (predicate(element)) return true
return false
}
사실 어떻게 동작하는지는 방식이 2개가 똑같습니다
all은 조건이 만족한다면, 모든 원소들을 순회하면서 predicate이 맞는지 검사할 것이고,
any는 조건이 하나의 원소라도 predicate을 만족한다면, 즉시 만족한다고 true를 리턴하게 됩니다
자주 쓰이지는 않지만, 가끔 쓰이는 유틸들
원소들을 순회하면서 어떤 기준으로 count할 것인지 결정하는
fold 라는 연산이 있습니다
public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
var accumulator = initial
for (element in this) accumulator = operation(accumulator, element)
return accumulator
}
초기값 = initial 을 명시하여 원소들을 순회하면서 operation을 실행하게 되죠
가끔씩 아주 정렬하고 싶을 때가 있습니다
바로 sortedBy 입니다
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
return sortedWith(compareBy(selector))
}
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
if (this is Collection) {
if (size <= 1) return this.toList()
@Suppress("UNCHECKED_CAST")
return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList()
}
return toMutableList().apply { sortWith(comparator) }
}
어떤 기준에 의해서 오름차순으로 정렬할 것인지 행동하게 됩니다
정리
우리는 kotlin collection의 기본 util 함수에 대해서 살펴보았는데요.
처음에 언급하였듯이 Sequence와 forEach 대해서는 살펴보지 않았습니다
왜 일까요.? Sequence는 비교적 특이한 성질을 가지고 있기 때문인데요.
글을 쓰기 시작하면 엄청 길어질 것 같아서 ㅎㅎ 다음 게시글로 찾아뵙도록 하겠습니다
'Developer > Kotlin & Java' 카테고리의 다른 글
[Kotlin] Coroutine - 2. CoroutineScope & Context & Dispathcer 을 파헤쳐보자 (2) | 2022.03.01 |
---|---|
[Kotlin] Coroutine - 1. 코루틴에서 동시성이란? (5) | 2022.02.05 |
Kotlin High Function - 고차 함수 람다 함수에 대해 알아보자 (0) | 2021.02.11 |
Java - AES Cipher 를 이용한 대칭키 암호화 방식 (0) | 2021.01.29 |
Kotlin - Null 을 다루는 방법 / 체이닝 / lateinit (0) | 2021.01.16 |