[Kotlin] Collection 날먹하는 함수들

H43RO·2021년 10월 21일
7

Kotlin 과 친해지기

목록 보기
15/18
post-thumbnail

Kotlin 에서는 다양한 프로그래밍 언어와 다를 것 없이 여러가지 Collection 자료구조를 제공한다. 조금 특별한 점이라면 Mutable Collection 과 Immutable Collection 을 구분한다는 점이다.

코틀린을 활용해본 사람들이라면 Collection 자료구조인 List, Set, Map 등을 활용해보았을 것이다. 이번 포스팅에선 이러한 Collection 자료구조를 사용할 때 유용하게 접목해볼 수 있는 여러 기본 내장 함수를 소개하고자 한다. 필자는 개인적으로 코틀린의 Collection 관련 내장 함수들은 타 언어에 비해 정말 빵빵하다고 생각한다. 왜냐하면, 코틀린이라는 언어 자체가 고차함수를 지원하기 때문에 더욱 강력한 동작을 수행할 수 있기 때문이다. 같이 한 번 살펴보자.

sort()

대부분 언어가 갖고있는 내장 함수이다. 말 그대로 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)]

map()

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]

forEach()

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

filter()

해당 고차함수는 이름에서 알 수 있듯 특정 조건에 부합하는 요소만 걸러내서 새로운 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]

find()

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

any(), all(), none()

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 를 반환한다.

flatMap()

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 을 만들도록 했기 때문이다.


partition()

어떤 원소에 대해 특정 조건을 걸어서, 조건에 부합하는 녀석들과 부합하지 않는 녀석들 이렇게 두 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]

getOrElse()

이 녀석은 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() 에 의해 출력된 것이다. ㄹㅇㅋㅋ


reduce(), fold()

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) 라는 표현식을 통해 누적합을 쌓아가게 된다.

fold() 의 결과가 18이 나온 과정

  1. acc : 0, i : 1 → 0 + (1 * 2) = 2
  2. acc : 2, i : 3 → 2 + (3 * 2) = 8
  3. acc : 8, i = 5 → 8 + (5 * 2) = 18

reduce() 의 결과가 17이 나온 과정

  1. acc : 1, i : 3 → 1 + (3 * 2) = 7
  2. acc : 7, i = 5 → 7 + (5 * 2) = 17

이번 포스팅에선 함수형 프로그래밍 언어인만큼 고차함수를 활용하여 빵빵하게 지원되는 코틀린의 Collection 내장 함수 몇 가지에 대해 살펴보았다. 정말 유용하고 다양한 구현을 쉽게 해볼 수 있어 편리한 것 같다.

profile
어려울수록 기본에 미치고 열광하라

1개의 댓글

comment-user-thumbnail
2022년 5월 9일

깔끔하게 정리되어있네요
코테 문제 풀 때 자주 볼 거같아요

답글 달기