컬렉션 정렬
컬렉션 정렬 (Ordering)
컬렉션 타입에서 요소의 순서는 중요한 측면이에요. 예를 들어 같은 요소를 가진 두 리스트라도 요소가 다르게 정렬되어 있으면 서로 같지 않아요.
Kotlin에서 객체의 순서는 여러 방식으로 정의될 수 있어요. 먼저 자연 순서(natural order)가 있어요. 이는 Comparable 인터페이스의 구현을 위해 정의돼요. 자연 순서는 다른 순서가 지정되지 않았을 때 정렬에 사용돼요.
대부분의 내장 타입은 비교가 가능해요(comparable).
- 숫자 타입은 전통적인 수치 순서를 사용해요:
1은0보다 크고,-3.4f는-5f보다 크죠. Char와String은 사전식 순서를 사용해요:b는a보다 크고,world는hello보다 크죠.
사용자 정의 타입에 자연 순서를 정의하려면 그 타입을 Comparable의 구현자로 만들어요. 이는 compareTo() 함수를 구현해야 함을 요구해요. compareTo()는 같은 타입의 다른 객체를 인자로 받고, 어떤 객체가 더 큰지 보여주는 정수 값을 반환해야 해요.
- 양수 값은 리시버 객체가 더 크다는 뜻이에요.
- 음수 값은 인자보다 작다는 뜻이에요.
- 0은 객체가 서로 같다는 뜻이에요.
다음은 major와 minor 부분으로 이루어진 버전을 정렬하는 클래스예요.
출처: Kotlin 공식 문서
본문
class Version(val major: Int, val minor: Int): Comparable<Version> {
override fun compareTo(other: Version): Int = when {
this.major != other.major -> this.major compareTo other.major // 중위 형태의 compareTo()
this.minor != other.minor -> this.minor compareTo other.minor
else -> 0
}
}
fun main() {
println(Version(1, 2) > Version(1, 3))
println(Version(2, 0) > Version(1, 5))
}
사용자 정의 순서(custom order)는 어떤 타입의 인스턴스든 원하는 방식으로 정렬할 수 있게 해 줘요. 특히 비교 불가능한 객체에 순서를 정의하거나, 비교 가능한 타입에 대해 자연 순서가 아닌 다른 순서를 정의할 수 있어요. 타입에 사용자 정의 순서를 정의하려면 그 타입에 대한 Comparator를 만들어요. Comparator는 compare() 함수를 담고 있는데, 이 함수는 클래스의 두 인스턴스를 받아 두 인스턴스 사이의 비교 결과를 정수로 반환해요. 그 결과는 위에서 설명한 compareTo()의 결과와 같은 방식으로 해석돼요.
fun main() {
//sampleStart
val lengthComparator = Comparator { str1: String, str2: String -> str1.length - str2.length }
println(listOf("aaa", "bb", "c").sortedWith(lengthComparator))
//sampleEnd
}
lengthComparator가 있으면 기본 사전식 순서 대신 문자열 길이로 문자열을 정렬할 수 있어요.
Comparator를 정의하는 더 짧은 방법은 표준 라이브러리의 compareBy() 함수를 쓰는 거예요. compareBy()는 인스턴스에서 Comparable 값을 만들어 내는 람다를 받고, 만들어진 값들의 자연 순서를 사용자 정의 순서로 정의해요.
compareBy()를 쓰면 위 예시의 길이 비교자는 이렇게 생겨요.
fun main() {
//sampleStart
println(listOf("aaa", "bb", "c").sortedWith(compareBy { it.length }))
//sampleEnd
}
또한 여러 기준에 기반한 순서를 정의할 수도 있어요. 예를 들어 문자열을 길이로 정렬하고 길이가 같을 때는 알파벳 순으로 정렬하려면 이렇게 쓸 수 있어요.
fun main() {
//sampleStart
val sortedStrings = listOf("aaa", "bb", "c", "b", "a", "aa", "ccc")
.sortedWith { a, b ->
when (val compareLengths = a.length.compareTo(b.length)) {
0 -> a.compareTo(b)
else -> compareLengths
}
}
println(sortedStrings)
// [a, b, c, aa, bb, aaa, ccc]
//sampleEnd
}
여러 기준으로 정렬하는 것은 흔한 시나리오이기 때문에, Kotlin 표준 라이브러리는 보조 정렬 규칙을 추가하는 데 사용할 수 있는 .thenBy() 함수를 제공해요.
예를 들어 compareBy()를 .thenBy()와 결합하면 이전 예시처럼 문자열을 먼저 길이로, 그다음 알파벳 순으로 정렬할 수 있어요.
fun main() {
//sampleStart
val sortedStrings = listOf("aaa", "bb", "c", "b", "a", "aa", "ccc")
.sortedWith(compareBy<String> { it.length }.thenBy { it })
println(sortedStrings)
// [a, b, c, aa, bb, aaa, ccc]
//sampleEnd
}
Kotlin 컬렉션 패키지는 컬렉션을 자연 순서, 사용자 정의 순서, 심지어 무작위 순서로 정렬하는 함수를 제공해요. 이 페이지에서는 읽기 전용 컬렉션에 적용되는 정렬 함수를 설명할게요. 이 함수들은 원래 컬렉션의 요소를 요청된 순서로 담은 새 컬렉션을 결과로 반환해요. 변경 가능한 컬렉션을 제자리에서 정렬하는 함수를 배우려면 List 특화 연산을 참고하세요.
자연 순서
기본 함수 .sorted()와 .sortedDescending()는 컬렉션의 요소를 자연 순서에 따라 오름차순과 내림차순으로 정렬한 값을 반환해요. 이 함수들은 Comparable 요소의 컬렉션에 적용돼요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
println("Sorted ascending: ${numbers.sorted()}")
println("Sorted descending: ${numbers.sortedDescending()}")
//sampleEnd
}
사용자 정의 순서
사용자 정의 순서로 정렬하거나 비교 불가능한 객체를 정렬하려면 .sortedBy()와 .sortedByDescending() 함수가 있어요. 이 함수들은 컬렉션 요소를 Comparable 값으로 매핑하는 선택자 함수를 받고, 그 값들의 자연 순서로 컬렉션을 정렬해요.
fun main() {
//sampleStart
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")
//sampleEnd
}
컬렉션 정렬을 위한 사용자 정의 순서를 정의하려면 자신만의 Comparator를 제공할 수 있어요. 이렇게 하려면 .sortedWith() 확장 함수를 호출하면서 Comparator를 전달해요. 이 함수를 쓰면 문자열을 길이로 정렬하는 코드는 이렇게 보여요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
println("Sorted by length ascending: ${numbers.sortedWith(compareBy { it.length })}")
//sampleEnd
}
정렬된 순서 확인
요소가 이미 지정된 순서를 따르는지 확인하려면 다음 확장 함수를 사용할 수 있어요.
.isSorted().isSortedDescending().isSortedWith(comparator).isSortedBy(selector).isSortedByDescending(selector)
이 확장 함수들은 요소가 지정된 순서에 있거나 두 개 미만의 요소가 있으면 true를 반환해요. 순서가 틀린 쌍을 찾는 즉시 false를 반환하고 확인을 멈춰요.
HashSet처럼 반복 순서가 보장되지 않는 컬렉션에서는 호출할 때마다 결과가 달라질 수 있어요. 일관된 순서로 요소를 만들지 않는 시퀀스에도 마찬가지예요. 호출할 때마다 같은 결과를 얻으려면 List처럼 반복 순서가 보장되는 컬렉션에서만 이 함수들을 사용해요.
Double과 Float 값을 확인할 때 이 함수들은 NaN을 다른 어떤 값보다 큰 것으로, -0.0을 0.0보다 작은 것으로 취급해요. 추가로 .isSortedBy()와 .isSortedByDescending() 함수는 null 선택자 결과를 non-null 값보다 작은 것으로 취급해요.
이 함수들을 시퀀스에서 호출하면 그 연산은 종단 연산이에요. 다른 시퀀스를 반환하는 대신, 그 시퀀스를 소비해 Boolean 값을 만들어요.
이 정렬 순서 함수들은 배열, 프리미티브 배열, 부호 없는 배열에도 사용할 수 있어요. 부호 없는 배열과 그에 대한 연산은 Experimental이며 @ExperimentalUnsignedTypes 애노테이션으로 opt-in이 필요해요.
.isSorted()와 .isSortedBy() 함수로 정렬 순서를 확인하는 예시를 볼게요.
data class User(val name: String, val age: Int)
fun main() {
//sampleStart
val numbers = listOf(1, 2, 3, 4)
println(numbers.isSorted())
// true
val users = listOf(
User("Alice", 24),
User("Bob", 31),
User("Charlie", 29),
)
println(users.isSortedBy(User::age))
// false
val descending = listOf(4, 3, 2, 1)
println(descending.isSortedDescending())
// true
//sampleEnd
}
역순
.reversed() 함수를 사용해 컬렉션을 뒤집은 순서로 얻을 수 있어요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
println(numbers.reversed())
//sampleEnd
}
.reversed() 확장 함수는 요소의 복사본을 담은 새 컬렉션을 반환해요. 그래서 원본 컬렉션을 나중에 변경해도, 이전에 얻은 .reversed()의 결과에는 영향을 주지 않아요.
또 다른 뒤집기 함수 .asReversed()는 같은 컬렉션 인스턴스의 뒤집힌 뷰를 반환해요. 그래서 원본 리스트가 변경되지 않을 것이라면 .reversed()보다 더 가볍고 선호될 수 있어요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)
//sampleEnd
}
원본 리스트가 변경 가능하다면, 그 모든 변경이 뒤집힌 뷰에 반영되고 그 반대도 마찬가지예요.
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)
numbers.add("five")
println(reversedNumbers)
//sampleEnd
}
하지만 리스트가 변경 가능한지 알 수 없거나 소스가 리스트가 아니라면, .reversed()가 더 선호돼요. 그 결과는 미래에 변경되지 않을 복사본이기 때문이에요.
무작위 순서
마지막으로 컬렉션 요소를 무작위 순서로 담은 새 List를 반환하는 함수가 있어요 — .shuffled(). 인자 없이 호출하거나 Random 객체와 함께 호출할 수 있어요.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
println(numbers.shuffled())
//sampleEnd
}