Kotlin 에서는 다양한 프로그래밍 언어와 다를 것 없이 여러가지 Collection 자료구조를 제공한다. 조금 특별한 점이라면 Mutable Collection 과 Immutable Collection 을 구분한다는 점이다.
코틀린을 활용해본 사람들이라면 Collection 자료구조인 List, Set, Map 등을 활용해보았을 것이다. 이번 포스팅에선 이러한 Collection 자료구조를 사용할 때 유용하게 접목해볼 수 있는 여러 기본 내장 함수를 소개하고자 한다. 필자는 개인적으로 코틀린의 Collection 관련 내장 함수들은 타 언어에 비해 정말 빵빵하다고 생각한다. 왜냐하면, 코틀린이라는 언어 자체가 고차함수를 지원하기 때문에 더욱 강력한 동작을 수행할 수 있기 때문이다. 같이 한 번 살펴보자.
대부분 언어가 갖고있는 내장 함수이다. 말 그대로 Collection 을 정렬해주는 역할을 수행한다. sorted()
를 활용하면 정렬이 된 새로운 객체를 반환한다. 그리고 내림차순 정렬의 경우 sortByDescending()
등을 사용할 수 있다.
fun main() {
val a = mutableListOf(3, 2, 1)
a.sort() // 오름차순 정렬
println(a)
val sorted = a.sortedByDescending { it } // 내림차순, 정렬된 새로운 Collection 객체 반환
println(sorted)
// sortBy() : 각 객체가 갖고있는 프로퍼티를 기준으로 정렬
val list = mutableListOf(1 to "A", 2 to "B", 100 to "C", 50 to "D", 10 to "E")
list.sortBy { it.second }
println(list)
}
함수형 프로그래밍 언어답게 sortBy()
라는 고차함수를 통해 Collection 을 구성하는 각 객체들의 특정 프로퍼티를 기준으로 정렬을 할 수 있도록 해준다. 위 예제에서는 Pair
객체의 second
값을 기준으로 정렬하게 된다. 따라서 위 예제를 실행해보았을 때, 아래와 같은 결과를 보인다.
[1, 2, 3]
[3, 2, 1]
[(1, A), (2, B), (100, C), (50, D), (10, E)]
Collection 에 사용할 수 있는 고차 함수이다. Collection 을 구성하는 각 요소들에 대해 특정 표현식에 의거하여 변형을 거친 뒤 새로운 Collection 을 반환해준다.
fun main() {
val a: List<Int> = listOf(1, 2, 3)
val b = a.map { it * 10 }
println(b)
}
각 요소들에 대해 10
을 곱해주도록 매핑시켜줬다. it
키워드를 통해 각 요소의 값을 접근할 수 있다. 따라서 아래와 같은 결과를 보여준다.
[10, 20, 30]
Collection 을 구성하는 요소들을 예쁘게(?) 하나씩 순회할 수 있다. forEachIndexed()
라는 고차함수 역시 제공하는데, 이는 Python 의 enumerate()
와 같은 동작을 수행한다. 즉, 각 요소들의 값 뿐만 아니라 인덱스도 함께 사용할 수 있도록 해주는 녀석이다.
fun main() {
val a: List<Char> = listOf('A', 'B', 'C')
a.forEach {
println(it)
}
a.forEachIndexed { index, c ->
println("$index : $c")
}
}
A
B
C
0 : A
1 : B
2 : C
해당 고차함수는 이름에서 알 수 있듯 특정 조건에 부합하는 요소만 걸러내서 새로운 Collection 을 반환해주는 녀석이다. Boolean 값을 반환하는 표현식을 주입해준다.
fun main() {
val a: List<Int> = listOf(1, 2, 3, 4, 5, 6)
val b = a.filter {
it % 2 == 0
}
println(b)
}
위와 같이 짝수만 걸러내서 새로운 Collection 을 반환하는 동작을 구현할 수 있다.
[2, 4, 6]
filter()
는 조건에 맞는 모든 녀석들을 걸러내서 새로운 Collection 에 담아줬다고 한다면, find()
는 최초로 조건에 부합하는 녀석을 반환해주는 녀석이다. 만약 조건에 부합하는 녀석이 끝까지 발견되지 않는다면 null
을 반환한다는 특징이 있다.
fun main() {
val a: List<Int> = listOf(1, 2, 3, 4, 5, 6)
val b = a.find {
it % 2 == 0
}
val c = a.findLast {
it % 2 == 0
}
println(b)
println(c)
}
반대로 findLast()
라는 친구는 조건에 부합하는 녀석들 중 가장 마지막 녀석을 반환해준다.
2
6
Collection 각 구성 요소들을 하나씩 검사해보며 Boolean 을 반환하는 함수들이다. 아래를 통해 각 함수들이 어떤 것을 검사하는지 익힐 수 있을 것이다.
fun main() {
val a: List<Int> = listOf(1, 2, 3, 4, 5, 6)
if (a.any { it % 2 == 0 }) {
println("짝수 데이터가 존재합니당")
}
if (a.all { it % 2 == 0 }) {
println("모두 짝수 데이터입니다!")
} else {
println("홀수 데이터도 섞여있네용..")
}
if (a.none { it > 7 }) {
println("7보다 큰 원소가 없습니당")
}
}
any()
는 조건을 만족하는 녀석이 하나라도 있다면 true
를 반환하는 녀석이다.all()
은 모든 녀석이 조건을 만족할 때 true
를 반환한다.none()
은 모든 녀석이 조건을 만족하지 않을 때 true
를 반환한다.RxJava 에서도 볼 수 있는 연산자인데, 매우 비슷한 동작을 수행한다. Collection 을 구성하는 요소들 각각마다 원하는 형태로 Collection 을 새로 만들고, 이들을 하나의 Collection 으로 Flatten 하여 반환한다.
fun main() {
val a: List<Int> = listOf(1, 2, 3)
val flatA = a.flatMap {
listOf(it * 3, it * 4)
}
println(flatA)
}
[3, 4, 6, 8, 9, 12]
위 동작을 풀어쓰자면, listOf(3, 4)
와 listOf(6, 8)
과 listOf(9, 12)
가 하나의 List 로 Flatten 된 것이다. 각 원소에 대해 listOf(it * 3, it * 4)
형태로 새로운 Collection 을 만들도록 했기 때문이다.
어떤 원소에 대해 특정 조건을 걸어서, 조건에 부합하는 녀석들과 부합하지 않는 녀석들 이렇게 두 Collection 으로 분리해준다. 이 때 Pair
형태로 분리되게 되는데, 조건에 부합하는 녀석들이 first
로 가고 아닌 녀석들을 second
로 간다.
fun main() {
val a: List<Int> = listOf(1, 2, 3, 4, 5, 6)
val partition = a.partition { it % 2 == 0 }
println(partition.first) // 조건에 만족하는 녀석들
println(partition.second) // 조건에 만족하지 않는 녀석들
}
[2, 4, 6]
[1, 3, 5]
이 녀석은 Collection 에 인덱스로 값을 참조했을 때, 만약 해당 인덱스에 값이 없을 경우 지정된 스코프 내에서 원하는 동작들을 수행할 수 있고, 스코프 내 가장 마지막 줄 코드의 반환값을 뱉는다.
fun main() {
val a: List<Int> = listOf(1, 2, 3, 4, 5, 6)
println(a.getOrElse(2) { 10 })
println(a.getOrElse(10) {
println("10번째 원소가 없습니다")
"ㄹㅇㅋㅋ"
})
}
위 코드를 실행하면 어떤 결과를 뱉을까?
3
10번째 원소가 없습니다
ㄹㅇㅋㅋ
이러한 결과가 나타난 이유는, 우선 2로 인덱싱했을 때 3이란 값이 담겨 있으니 정상적으로 반환됐고, 10으로 인덱싱했을 땐 값이 없으니 스코프로 진입한다. 이 때 "10번째 원소가 없습니다" 라는 문구를 출력하는 코드가 실행됐고, 맨 마지막 "ㄹㅇㅋㅋ" 라는 String 이 반환되어 이것이 가장 바깥 println()
에 의해 출력된 것이다. ㄹㅇㅋㅋ
Collection 을 구성하는 모든 원소들에 대해 누적합을 계산하는 함수들이다. 고차함수이기 때문에 누적합을 어떻게 쌓아올리는 지에 대해 표현식을 걸어줄 수 있다. fold()
의 경우 초기값을 설정해줄 수 있다. reduce()
는 첫 번째 요소를 acc 로 사용하고, 두 번째 요소 부터 연산하게 된다.
fun main() {
val a: List<Int> = listOf(1, 3, 5)
println("Fold : ${a.fold(0) { acc, i ->
acc + i * 2
}}")
println("Reduce : ${a.reduce { acc, i ->
acc + i * 2
}}")
}
Fold : 18
Reduce : 17
위 예제의 경우 현재 누적합 + (현재 값 * 2)
라는 표현식을 통해 누적합을 쌓아가게 된다.
이번 포스팅에선 함수형 프로그래밍 언어인만큼 고차함수를 활용하여 빵빵하게 지원되는 코틀린의 Collection 내장 함수 몇 가지에 대해 살펴보았다. 정말 유용하고 다양한 구현을 쉽게 해볼 수 있어 편리한 것 같다.
깔끔하게 정리되어있네요
코테 문제 풀 때 자주 볼 거같아요