Programming/Kotlin

코틀린(Kotlin) - 컬렉션 API

JunsuKim 2021. 10. 17.
728x90

함수형 프로그래밍 스타일을 사용하면 컬렉션을 다룰 때 편리하다.

대부분의 작업에 라이브러리 함수를 활용할 수 있어 코드를 아주 간결하게 만들 수 있다.

컬렉션을 다루는 코틀린 표준 라이브러리를 알아보도록 하자.

filter와 map

이 두 함수는 컬렉션을 활용할 때 기반이 되는 함수다.

대부분의 컬렉션 연산을 이 두 함수를 통해 표현할 수 있다.

filter 함수는 컬렉션을 이터레이션 하면서 주어진 람다에 각 원소를 넘겨 람다가 true를 반환하는 원소만 모은다.

val list = istOf(1, 2, 3, 4)
println(list.filter{ it % 2 == 0 })

[2, 4]

결과는 입력 컬렉션의 원소 중 주어진 술어를 만족하는 원소만으로 이뤄진 새로운 컬렉션이다.

filter 함수는 컬렉션에서 원치 않는 원소를 제거할 수는 있지만, 원소를 변환할 수는 없다.

 

map 함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아 새 컬렉션을 만든다.

val list = listOf(1, 2, 3, 4)
println(list.map{ it * it })

[1, 4, 9, 16]

결과를 보면 원본 리스트의 원소의 개수는 같지만, 각 원소는 주어진 함수에 따라 변환된 새로운 컬렉션이다.

all, any, count, find

우선 어떤 사람의 나이가 27살 이하인지 판단하는 술어 함수 canBeInClub27을 만들었다.

val canBeInClub27 = {p: Person, p.age <= 27}

이 함수를 통해 all, any, count, find의 역할을 보도록 하겠다.

all과 any는 컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산을 한다.

모든 원소가 이 술어를 만족하는지 궁금하다면 any를 쓰면 된다.

val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.all(canBeInClub27))

false

술어가 만족하는 원소가 하나라도 있는지가 궁금하다면 any를 쓴다.

val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.any(canBeInClub27))

true

드 모르강의 법칙에 따르면 !all을 수행한 결과와 그 조건의 부정에 대해 any를 수행한 결과는 같고, 또한 어떤 조건에 !any를 수행한 결과와 그 조건의 부정에 대해 all을 수행한 결과도 같다.

가독성을 높이기 위해서는 all, any 앞에 !를 붙이지 않는 것이 좋다.

 

count 함수는 조건을 만족하는 원소의 개수를 반환한다.

val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.count(canBeInClub27))

1

count를 대신해 filter와 size를 통해 조건을 만족하는 개수를 구하는 방법도 있다.

println(people.filter(canBeInClub27).size)

하지만 이렇게 처리하면 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생겨 효율적이지 못하다.

 

find 함수는 조건을 만족하는 첫 번째 원소를 반환한다.

val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.all(canBeInClub27))

Person(name=Alice, age=27)

이 식은 조건을 만족하는 원소가 하나라도 있는 경우 가장 먼저 조건을 만족한다고 확인된 원소를 반환하며,

만족하는 원소가 전혀 없다면 null을 반환한다.

find는 firstOrNull과 같다. 조건을 만족하는 원소가 없으면 null이 나온다는 사실을 더 명확히 하고 싶다면 firstOrNull을 쓰면 된다.

groupBy

groupBy 함수는 컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나눠주는 역할을 한다.

val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
println(people.groupBy{ it.age })


{29=[Person(name=Bob, age=29)], 31=[Person(name=Alice, age=31], Person(name=Carol, age=31)]}

이 연산의 결과는 컬렉션의 원소를 구분하는 특성이 키(key)이고, 키 값에 따른 각 그룹이 값(value)인 맵이다.

출력 결과를 보면 각 그룹은 리스트 형태이다.

이는 groupBy의 결과 타입이 Map<Int, List<Person>>인 것을 알 수 있다.

필요에 따라 mapKeys나 mapValues 등을 사용해 변경이 가능하다.

flatMap과 flatten

Book으로 표현한 책에 대한 정보를 저장하는 도서관이 있다고 하자.

class Book(val title: String, val authors: List<String>)

책마다 저자가 한명 또는 여러 명이 있을 때, 도서관에 있는 책의 저자를 모두 모은 집합을 가져오려면 다음과 같이 하면 된다.

books.flatMap{ it.authors }.toSet()

flatMap 함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고, 람다를 적용한 결과에서 얻어지는 여러 리스트를 한 리스트로 모은다(또는 펼친다(flatten)).

예를 들어보자.

val strings = listOf("abc", "def")
println(strings.flatMap { it.toList() })

{a, b, c, d, e, f}

리스트의 리스트가 있는데 모든 중첩된 리스트의 원소를 한 리스트로 모아야 한다면 flatMap을 떠올릴 수 있을 것이다.

하지만 특별히 반환해야 할 내용이 없다면 flatten 함수를 사용할 수 있다.

listOfLists.flatten()
728x90

댓글