일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- AWS
- 프로그래머스
- Gradle
- androidStudio
- Coroutine
- Kotlin
- 백준
- MiTweet
- boj
- Compose
- 암호학
- Coroutines
- 쿠링
- activity
- ProGuard
- Rxjava
- Hilt
- TEST
- Codeforces
- relay
- MyVoca
- android
- GitHub
- livedata
- architecture
- 코드포스
- pandas
- 코루틴
- textfield
- Python
- Today
- Total
이동식 저장소
[Kotlin] Collections 확장 함수 본문
코틀린에는 List
, Set
등 다양한 collections 클래스가 존재하고, 또 다양한 확장 함수를 지원한다. 확장 함수를 적절히 사용하면 코드를 더 간결하고 읽기 쉽게 작성할 수 있다.
목차
- Ordering (순서)
- Sorting
- Reverse Order
- Random Order (shuffling)
- Aggregate (요약)
- Aggregations
- Fold, Reduce
- Transformations (변환)
- Mapping
- Zipping
- Association
- Flattening
- String Representation
- Filtering (검색)
- Filtering by predicate
- Partitioning
- Testing predicates
- 참고문헌
Ordering (순서)
Sorting
sorted()
, sortedDescending()
을 이용하면 컬렉션을 정렬한 결과를 반환받을 수 있다. 이때 원본 객체는 정렬되지 않는다.
val numbers = listOf("one", "two", "three", "four")
println("Sorted ascending: ${numbers.sorted()}")
println("Sorted descending: ${numbers.sortedDescending()}")
/*
Sorted ascending: [four, one, three, two]
Sorted descending: [two, three, one, four]
*/
문자열의 정렬은 사전 순을 따른다.
sortedBy()
와 sortedByDescending()
을 사용하여 정렬 조건을 지정할 수도 있다. 조건은 람다식으로 작성하며, 람다식의 반환 결과를 참고하여 오름차순 또는 내림차순으로 정렬한다.
val numbers = listOf("one", "two", "three", "four")
val sortedNumbers = numbers.sortedBy { it.length }
println("Sorted by length ascending: $sortedNumbers")
val sortedByLast = numbers.sortedByDescending { it.last() }
println("Sorted by the last letter descending: $sortedByLast")
/*
Sorted by length ascending: [one, two, four, three]
Sorted by the last letter descending: [four, two, one, three]
*/
정렬 조건을 람다식 말고 Comparator
객체로 지정할 수도 있다. compareBy()
함수를 이용하여 Comparator
객체를 만들 수 있다.
val numbers = listOf("one", "two", "three", "four")
println("Sorted by length ascending: ${numbers.sortedWith(compareBy { it.length })}")
/*
Sorted by length ascending: [one, two, four, three]
*/
Mutable한 객체의 경우, 객체 자체를 정렬할 수 있다. sorted*
대신 sort*
함수를 사용하면 된다.
val numbers = mutableListOf("one", "two", "three", "four")
numbers.sort()
println("Sort into ascending: $numbers")
numbers.sortDescending()
println("Sort into descending: $numbers")
numbers.sortBy { it.length }
println("Sort into ascending by length: $numbers")
numbers.sortByDescending { it.last() }
println("Sort into descending by the last letter: $numbers")
numbers.sortWith(compareBy<String> { it.length }.thenBy { it })
println("Sort by Comparator: $numbers")
/*
Sort into ascending: [four, one, three, two]
Sort into descending: [two, three, one, four]
Sort into ascending by length: [two, one, four, three]
Sort into descending by the last letter: [four, two, one, three]
Sort by Comparator: [one, two, four, three]
*/
Reverse Order
reversed()
함수를 사용하여 컬렉션을 뒤집을 수 있다. 원본 객체는 뒤집지 않으며, 뒤집어진 새로운 객체를 반환하므로 원본을 바꾸고 싶지 않을 때 사용하자.
val numbers = listOf("one", "two", "three", "four")
println(numbers.reversed())
/*
[four, three, two, one]
*/
asReversed()
를 사용하면 새 객체를 만드는 게 아니라 객체를 순회하는 순서만 반대로 볼 수 있다. 객체를 새로 만들지 않으므로 더 가벼우며, 원본 객체가 바뀌지 않을 때 사용하면 좋다.
val numbers = listOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)
/*
[four, three, two, one]
*/
Mutable한 객체의 경우 객체 자체를 뒤집을 수 있다. reversed()
를 사용하면 객체가 뒤집어지며, asReversed()
를 사용하면 원본의 변경 사항이 뒤집어진 복사본에도 반영된다. 물론 그 반대도 성립한다.
val numbers = mutableListOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)
numbers.add("five")
println(reversedNumbers)
/*
[four, three, two, one]
[five, four, three, two, one]
*/
Random order (shuffling)
shuffled()
을 사용하면 원소의 순서를 임의로 섞은 List
를 반환받을 수 있다.
val numbers = listOf("one", "two", "three", "four")
println(numbers.shuffled())
/*
[three, one, two, four]
*/
Aggregate (요약)
Aggregations
컬렉션을 대표하는 값을 계산할 수 있다. 통계학에서 요약 통계량
이라고 불리는 최댓값, 최솟값 등을 쉽게 계산할 수 있다.
minOrNull()
,maxOrNull()
함수는 원소의 최솟값 또는 최댓값을 반환한다. 컬렉션이 비어 있다면null
을 반환한다.average()
는 원소의 평균을 계산한다. 정수형 컬렉션에만 적용할 수 있다.sum()
은 원소의 합을 계산한다. 마찬가지로 정수형 컬렉션에만 적용할 수 있다.count()
는 원소의 개수를 계산한다.size()
와 동일하다.
val numbers = listOf(6, 42, 10, 4)
println("Count: ${numbers.count()}")
println("Max: ${numbers.maxOrNull()}")
println("Min: ${numbers.minOrNull()}")
println("Average: ${numbers.average()}")
println("Sum: ${numbers.sum()}")
/*
Count: 4
Max: 42
Min: 4
Average: 15.5
Sum: 62
*/
요약할 때 특정 기준을 지정할 수도 있다.
maxByOrNull()
과minByOrNull()
은 람다식의 결과 중 최댓값과 최솟값을 찾는다. 컬렉션이 비어 있다면null
을 반환한다.maxWithOrNull()
과minWithOrNull()
은Comparator
객체를 받아서 최댓값 또는 최솟값을 찾는다. 컬렉션이 비어 있다면null
을 반환한다.
// 3으로 나눈 나머지가 가장 큰 원소는?
val numbers = listOf(5, 42, 10, 4)
val min3Remainder = numbers.minByOrNull { it % 3 }
println(min3Remainder)
// 길이가 가장 긴 원소는?
val strings = listOf("one", "two", "three", "four")
val longestString = strings.maxWithOrNull(compareBy { it.length })
println(longestString)
/*
42
three
*/
sum()
에도 함수를 적용할 수 있다. 함수를 인자로 받아서 각 원소의 함숫값의 합을 계산할 수 있다.
sumBy()
는Int
를 반환하는 함수를 받아서 각 원소에 함수를 적용한 결과를 합한다.sumByDouble()
는 위와 같지만, 합을Double
로 반환한다.
val numbers = listOf(5, 42, 10, 4)
println(numbers.sumBy { it * 2 })
println(numbers.sumByDouble { it.toDouble() / 2 })
/*
122
30.5
*/
Fold, Reduce
fold()
또는 reduce()
를 사용하여 특정 연산(람다식)을 순서대로 적용한 결과를 얻을 수 있다. fold()
는 초기값을 받으며, reduce()
는 초기값을 컬렉션의 첫 번째 원소로 설정하고 두 번째 원소부터 연산을 수행한다. 이러한 성질 때문에 똑같은 연산이라도 fold()
와 reduce()
의 결과가 다를 수 있다.
val numbers = listOf(5, 2, 10, 4)
val sum = numbers.reduce { total, element -> total + element }
println(sum)
val sumDoubled = numbers.fold(0) { total, element -> total + element * 2 }
println(sumDoubled)
// 첫 번째 원소는 2배로 더해지지 않음
val sumDoubledReduce = numbers.reduce { total, element -> total + element * 2 }
println(sumDoubledReduce)
/*
21
42
37
*/
foldRight()
또는 reduceRight()
을 사용하여 컬렉션의 맨 뒤부터 연산을 적용할 수 있다. 위에서처럼 단순한 합의 경우에는 차이가 없겠지만, pow
등의 경우에는 연산 결과가 달라질 수 있으니 주의하자.
foldIndexed()
또는 reducedIndexed()
을 사용하면 람다식에서 인덱스와 값을 동시에 인자로 받을 수 있다. foldRightIndexed()
와 reducedRightIndexed()
는 컬렉션의 맨 뒤에서부터 인덱스와 함께 연산을 수행한다.
val numbers = listOf(5, 2, 10, 4)
val sumEven = numbers.foldIndexed(0) { idx, total, element ->
if (idx % 2 == 0) total + element else total }
println(sumEven)
val sumEvenRight = numbers.foldRightIndexed(0) { idx, total, element ->
if (idx % 2 == 0) total + element else total }
println(sumEvenRight)
/*
15
15
*/
컬렉션이 비어있을 때 모든 reduce()
함수는 Exception
을 발생시킨다. 다음의 함수를 사용하면 컬렉션이 비어있을 때 null
을 반환받을 수 있다.
reduceOrNull()
reduceRightOrNull()
reduceIndexedOrNull()
reduceRightIndexedOrNull()
Transformations (변환)
파이썬을 알고 있다면 친숙한 이름이 많이 등장한다.
Mapping
Mapping
은 컬렉션 객체에 특정 연산을 적용하여 새로운 컬렉션을 만드는 것이다. 가장 단순한 함수로 map()
이 있다. map()
은 함수를 인자로 받아서 각 원소에 함수를 적용한 결과를 모아 새 컬렉션 객체를 만든다. mapIndexed()
를 사용하면 함수에서 원소의 인덱스를 추가로 사용할 수 있다.
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })
/*
[3, 6, 9]
[0, 2, 6]
*/
변환 과정에서 null
이 발생할 여지가 있는 경우, mapNotNull()
또는 mapIndexedNotNull()
을 사용하여 null
을 거를 수 있다.
val numbers = setOf(1, 2, 3)
println(numbers.mapNotNull { if ( it == 2) null else it * 3 })
println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx })
/*
[3, 9]
[2, 6]
*/
컬렉션 Map
을 mapping
할 때는 key 또는 value 하나에만 함수를 적용할 수 있다. 각각 mapKeys()
와 mapValues()
를 사용하면 된다.
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
println(numbersMap.mapKeys { it.key.toUpperCase() })
println(numbersMap.mapValues { it.value + it.key.length })
/*
{KEY1=1, KEY2=2, KEY3=3, KEY11=11}
{key1=5, key2=6, key3=7, key11=16}
*/
Zipping
Zipping
이란 두 개의 컬렉션에서 원소를 동시에 읽는 것이다. 두 객체의 i
번째 원소를 동시에 읽을 수 있다. zip()
을 사용하면 i
번째 원소를 Pair
로 묶은 List
객체가 반환된다.
주의: a.zip(b)
대신 a zip b
를 사용할 수도 있다.
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)
/*
[(red, fox), (brown, bear), (grey, wolf)]
*/
컬렉션의 길이가 서로 다른 경우, 더 짧은 컬렉션의 길이까지만 탐색하여 반환한다.
val twoAnimals = listOf("fox", "bear")
println(colors.zip(twoAnimals))
/*
[(red, fox), (brown, bear)]
*/
Pair
대신 다른 객체를 반환하게 할 수도 있다. 두 번째 매개변수로 함수를 주면 된다.
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"})
/*
[The Fox is red, The Bear is brown, The Wolf is grey]
*/
zip()
을 되돌리고 싶다면 unzip()
을 사용하면 된다. 단, Pair
만 unzip()
가능함에 주의하자. unzip()
은 각 Pair
의 첫 번째와 두 번째 원소끼리 리스트로 묶은 결과를 반환한다.
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip())
/*
([one, two, three, four], [1, 2, 3, 4])
*/
Association
Association
은 각 컬렉션의 원소를 key
로 하여 value
를 대응시키는 변환이다. Association
의 결과는 key
와 value
의 Map
이다.
가장 기본적인 함수로 associateWith()
가 있다. 함수를 매개변수로 받아서 각 원소를 key
로, 원소에 함수를 적용한 결과를 value
로 하는 Map
을 만든다.
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })
/*
{one=3, two=3, three=5, four=4}
*/
함수를 value
가 아닌 key
에 대응시킬 수도 있다. associateBy()
를 사용하면 각 원소를 value
로, 원소에 함수를 적용한 결과를 key
로 하는 Map
을 만들 수 있다. With
와 By
를 잘 구별하자.
key
와 value
를 특정 형식으로 지정할 수도 있다. 각각 keySelector
와 valueTransform
에 함수를 넘겨주면 된다.
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateBy { it.first().toUpperCase() })
println(numbers.associateBy(keySelector = { it.first().toUpperCase() }, valueTransform = { it.length }))
/*
{O=one, T=three, F=four}
{O=3, T=5, F=4}
*/
Flattening
Flattening
은 여러 개의 컬렉션을 하나의 컬렉션으로 모으는 연산이다. flatten()
을 사용하면 각 컬렉션을 List
로 변환한 후, 변환된 List
를 하나로 이어붙인다.
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten())
/*
[1, 2, 3, 4, 5, 6, 1, 2]
*/
복잡한 컬렉션(Map
등)을 이어붙일 때는 flatMap()
을 고려해 보자. flatMap()
은 인자로 함수를 넘겨받아서 각 원소에 함수를 map()
한 결과를 flatten()
한다.
val containers = listOf(
StringContainer(listOf("one", "two", "three")),
StringContainer(listOf("four", "five", "six")),
StringContainer(listOf("seven", "eight"))
)
println(containers)
println(containers.flatMap { it.values })
/*
[StringContainer(values=[one, two, three]), StringContainer(values=[four, five, six]), StringContainer(values=[seven, eight])]
[one, two, three, four, five, six, seven, eight]
*/
String Representation
컬렉션의 원소를 문자열로 보여줄 필요가 있을 때는 보통 toString()
을 사용하지만, 특정 포맷을 지정하고 싶다면 joinToString()
또는 joinTo()
를 사용해 보자.
두 함수는 공통적으로 각 원소를 문자열로 이어붙인다. 이때 각 원소를 구분할 구분자(delimiter
)를 지정할 수 있다. 보통 쉼표(,
)를 구분자로 많이 사용한다. joinToString()
은 문자열을 반환하며, joinTo()
는 문자열을 인자로 주어진 Appendable
객체 뒤에 덧붙인다.
val numbers = listOf("one", "two", "three", "four")
println(numbers)
println(numbers.joinToString()) // 기본값: ", "
println(numbers.joinToString("|"))
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString)
println(listString)
/*
[one, two, three, four]
one, two, three, four
one|two|three|four
The list of numbers: one, two, three, four
*/
Filtering (검색)
Filtering
은 컬렉션에서 조건을 만족하는 특정 원소만 걸러내는 작업이다. 컬렉션으로 하는 대부분의 작업은 아마 filtering
일 것이다. 아래에서 소개할 함수는 공통적으로 predicate
함수를 받아서, 함수의 결과값이 true
인 원소와 false
인 원소를 나눈다. 모든 filtering
함수는 원본을 수정하지 않는다.
predicate
: 원소가 특정 조건을 만족하면true
를, 만족하지 않으면false
를 반환하는 람다 함수
보통은 filtering
한 결과에 map()
등 계속 다른 함수를 이어붙여(function chaining
) 작업을 수행한다.
Filtering by predicate
가장 기본적인 filtering
함수는 filter()
이다. filter()
는 predicate
가 지정하는 조건에 맞는 원소만 골라내 반환한다. List
와 Set
의 결과는 List
이고, Map
의 결과는 Map
이다.
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap)
/*
[three, four]
{key11=11}
*/
predicate
에서 인덱스를 사용하고 싶다면 filterIndexed()
를 사용하자.
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filterIndexed { index, value -> value.length > 3 }
println(longerThan3)
/*
[three, four]
*/
조건을 만족하지 않는 원소를 찾고 싶다면 filterNot()
을 사용하자.
val numbers = listOf("one", "two", "three", "four")
val filteredNot = numbers.filterNot { it.length <= 3 }
println(filteredNot)
/*
[three, four]
*/
filterIsInstance()
는 특정 타입의 원소만 걸러낼 수 있다. List<Any>
객체에서 filterIsInstance<T>()
를 호출하면 List<T>
를 반환하고, 반환된 리스트에 메소드를 이어붙일 수 있다.
val numbers = listOf(null, 1, "two", 3.0, "four")
println("All String elements in upper case:")
numbers.filterIsInstance<String>().forEach {
println(it.toUpperCase())
}
/*
All String elements in upper case:
TWO
FOUR
*/
filterNotNull()
은 null
이 아닌 원소만 걸러낸다. List<T?>
객체에서 filterNotNull()
을 호출하면 List<T: Any>
를 반환하므로 여기에도 메소드를 계속 이어붙일 수 있다.
val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
println(it.length) // nonnull 문자열의 길이만 출력
}
/*
3
3
*/
Partitioning
partition()
함수를 사용하면 조건을 만족하는 원소와 만족하지 않는 원소를 모두 돌려받을 수 있다. 보통은 partition()
의 결과를 unzip하여 받는다.
방금 말한 unzip은 코틀린의 unzip()
과는 다릅니다. 여기서 말하는 unzip은 Pair
의 원소를 다음과 같이 받는 방법을 의미합니다.
val numbers = listOf("one", "two", "three", "four")
val (match, rest) = numbers.partition { it.length > 3 } // unzip
println(match)
println(rest)
/*
[three, four]
[one, two]
*/
Testing predicates
컬렉션에 조건을 만족하는 원소가 있는지 없는지도 검사할 수 있다.
any()
: 조건을 만족하는 원소가 하나라도 있다면true
를 반환한다.none()
: 조건을 만족하는 원소가 하나도 없을 때true
를 반환한다.all()
: 모든 원소가 조건을 만족할 때true
를 반환한다. 컬렉션이 비어 있다면all()
은 항상true
를 반환한다. 이것을vacuous truth
라고 부른다.
val numbers = listOf("one", "two", "three", "four")
println(numbers.any { it.endsWith("e") }) // true
println(numbers.none { it.endsWith("a") }) // true
println(numbers.all { it.endsWith("e") }) // false
println(emptyList<Int>().all { it > 5 }) // true (vacuous truth)
참고문헌
Ordering - Kotlin Programming Language (kotlinlang.org)
Aggregate Operations - Kotlin Programming Language (kotlinlang.org)
Collection Transformation Operations - Kotlin Programming Language (kotlinlang.org)
Filtering Collections - Kotlin Programming Language (kotlinlang.org)
'Primary > Kotlin' 카테고리의 다른 글
[Kotlin] Coroutines - Basics (0) | 2021.01.19 |
---|---|
[Kotlin] Sequence (0) | 2021.01.15 |
[Kotlin] Thread 생성 및 실행 (0) | 2021.01.14 |
[Kotlin] 데이터 클래스 (0) | 2021.01.08 |
[Kotlin] 위임 (0) | 2021.01.06 |