List 전용 연산

List 전용 연산 (List-specific operations)

List는 Kotlin에서 가장 인기 있는 내장 컬렉션 타입이에요. 리스트의 요소에 대한 인덱스 접근은 리스트에 강력한 연산 집합을 제공해 줍니다.

출처: Kotlin 공식 문서

본문

인덱스로 요소 가져오기

리스트는 요소를 가져오는 모든 일반 연산인 elementAt(), first(), last() 등을 지원해요. 나머지는 Retrieve single elements에서 확인할 수 있습니다. 리스트에서 특별한 점은 요소에 대한 인덱스 접근이 가능하다는 건데, 그래서 요소를 읽는 가장 간단한 방법은 인덱스로 가져오는 것이에요. get() 함수에 인덱스를 인자로 넘기거나, 축약 문법인 [index]로 처리합니다.

지정한 인덱스보다 리스트 크기가 작으면 예외가 발생해요. 이런 예외를 피하는 데 도움이 되는 함수가 두 가지 더 있어요.

  • getOrElse()는 컬렉션에 인덱스가 없을 때 반환할 기본 값을 계산하는 함수를 제공하게 해 줘요.
  • getOrNull()은 기본 값으로 null을 반환해요.
fun main() {
//sampleStart
    val numbers = listOf(1, 2, 3, 4)
    println(numbers.get(0))
    println(numbers[0])
    //numbers.get(5)                         // exception!
    println(numbers.getOrNull(5))             // null
    println(numbers.getOrElse(5, {it}))        // 5
//sampleEnd
}

리스트 부분 가져오기

Retrieving Collection Parts의 일반 연산에 더해, 리스트는 지정한 요소 범위를 리스트로 보여주는 뷰를 반환하는 subList() 함수를 제공해요. 따라서 원본 컬렉션의 요소가 바뀌면 이전에 만든 하위 리스트에서도 바뀌고, 그 반대도 마찬가지입니다.

fun main() {
//sampleStart
    val numbers = (0..13).toList()
    println(numbers.subList(3, 6))
//sampleEnd
}

요소 위치 찾기

선형 검색

어떤 리스트에서든 indexOf()lastIndexOf() 함수를 사용해 요소의 위치를 찾을 수 있어요. 이 함수들은 리스트에서 주어진 인자와 같은 요소의 첫 위치와 마지막 위치를 반환합니다. 그런 요소가 없으면 두 함수 모두 -1을 반환해요.

fun main() {
//sampleStart
    val numbers = listOf(1, 2, 3, 4, 2, 5)
    println(numbers.indexOf(2))
    println(numbers.lastIndexOf(2))
//sampleEnd
}

프레디케이트를 받아 일치하는 요소를 검색하는 함수 쌍도 있어요.

  • indexOfFirst()는 프레디케이트와 일치하는 첫 요소의 인덱스를 반환하고, 그런 요소가 없으면 -1을 반환해요.
  • indexOfLast()는 프레디케이트와 일치하는 마지막 요소의 인덱스를 반환하고, 그런 요소가 없으면 -1을 반환해요.
fun main() {
//sampleStart
    val numbers = mutableListOf(1, 2, 3, 4)
    println(numbers.indexOfFirst { it > 2})
    println(numbers.indexOfLast { it % 2 == 1})
//sampleEnd
}

정렬된 리스트에서의 이진 검색

리스트에서 요소를 검색하는 방법이 하나 더 있어요. 바로 이진 검색입니다. 이 방식은 다른 내장 검색 함수보다 훨씬 빠르지만, 리스트가 특정 순서(자연 순서 또는 함수 파라미터로 제공된 다른 순서)에 따라 오름차순으로 정렬되어 있어야 해요. 그렇지 않으면 결과가 정의되지 않습니다.

정렬된 리스트에서 요소를 검색하려면 값을 인자로 넘겨 binarySearch() 함수를 호출해요. 그런 요소가 있으면 함수는 그 인덱스를 반환하고, 없으면 (-insertionPoint - 1)을 반환합니다. 여기서 insertionPoint는 리스트가 정렬된 상태를 유지하도록 그 요소를 삽입해야 하는 인덱스예요. 주어진 값과 같은 요소가 둘 이상이면 검색은 그 인덱스들 중 아무거나 반환할 수 있어요.

검색할 인덱스 범위를 지정할 수도 있어요. 이 경우 함수는 두 지정 인덱스 사이에서만 검색합니다.

fun main() {
//sampleStart
    val numbers = mutableListOf("one", "two", "three", "four")
    numbers.sort()
    println(numbers)
    println(numbers.binarySearch("two"))  // 3
    println(numbers.binarySearch("z")) // -5
    println(numbers.binarySearch("two", 0, 2))  // -3
//sampleEnd
}

Comparator 이진 검색

리스트 요소가 Comparable이 아닐 때는 이진 검색에 사용할 Comparator를 제공해야 해요. 리스트는 이 Comparator에 따라 오름차순으로 정렬되어 있어야 합니다. 예시를 살펴볼게요.

data class Product(val name: String, val price: Double)

fun main() {
//sampleStart
    val productList = listOf(
        Product("WebStorm", 49.0),
        Product("AppCode", 99.0),
        Product("DotTrace", 129.0),
        Product("ReSharper", 149.0))

    println(productList.binarySearch(Product("AppCode", 99.0), compareBy<Product> { it.price }.thenBy { it.name }))
//sampleEnd
}

여기 Product 인스턴스 리스트는 Comparable이 아니고, 순서를 정의하는 Comparator가 있어요. p1의 가격이 p2의 가격보다 작으면 제품 p1이 제품 p2보다 앞섭니다. 그래서 이 순서에 따라 오름차순으로 정렬된 리스트가 있으면 binarySearch()로 지정한 Product의 인덱스를 찾을 수 있죠.

커스텀 Comparator는 리스트가 자연 순서와 다른 순서를 사용할 때도 유용해요. 예를 들어 String 요소의 대소문자를 구분하지 않는 순서 같은 경우죠.

fun main() {
//sampleStart
    val colors = listOf("Blue", "green", "ORANGE", "Red", "yellow")
    println(colors.binarySearch("RED", String.CASE_INSENSITIVE_ORDER)) // 3
//sampleEnd
}

비교 함수 이진 검색

비교 함수를 사용한 이진 검색은 명시적인 검색 값을 제공하지 않고 요소를 찾을 수 있게 해 줘요. 대신 요소를 Int 값에 매핑하는 비교 함수를 받아, 함수가 0을 반환하는 요소를 검색합니다. 리스트는 제공된 함수에 따라 오름차순으로 정렬되어 있어야 해요. 다시 말해 비교 함수의 반환 값은 한 요소에서 다음 요소로 갈수록 커져야 합니다.

import kotlin.math.sign
//sampleStart
data class Product(val name: String, val price: Double)

fun priceComparison(product: Product, price: Double) = sign(product.price - price).toInt()

fun main() {
    val productList = listOf(
        Product("WebStorm", 49.0),
        Product("AppCode", 99.0),
        Product("DotTrace", 129.0),
        Product("ReSharper", 149.0))

    println(productList.binarySearch { priceComparison(it, 99.0) })
}
//sampleEnd

Comparator 이진 검색과 비교 함수 이진 검색 모두 리스트 범위에 대해서도 수행할 수 있어요.

리스트 쓰기 연산

Collection write operations에 설명된 컬렉션 수정 연산에 더해, 가변(mutable) 리스트는 특정 쓰기 연산을 지원해요. 이런 연산은 인덱스를 사용해 요소에 접근함으로써 리스트 수정 능력을 넓힙니다.

추가

리스트의 특정 위치에 요소를 추가하려면 요소 삽입 위치를 추가 인자로 제공하며 add()addAll()을 사용해요. 위치 뒤에 오는 모든 요소는 오른쪽으로 이동합니다.

fun main() {
//sampleStart
    val numbers = mutableListOf("one", "five", "six")
    numbers.add(1, "two")
    numbers.addAll(2, listOf("three", "four"))
    println(numbers)
//sampleEnd
}

갱신

리스트는 주어진 위치의 요소를 교체하는 함수인 set()과 그 연산자 형태 []도 제공해요. set()은 다른 요소의 인덱스를 바꾸지 않습니다.

fun main() {
//sampleStart
    val numbers = mutableListOf("one", "five", "three")
    numbers[1] =  "two"
    println(numbers)
//sampleEnd
}

fill()은 간단히 컬렉션의 모든 요소를 지정한 값으로 교체해요.

fun main() {
//sampleStart
    val numbers = mutableListOf(1, 2, 3, 4)
    numbers.fill(3)
    println(numbers)
//sampleEnd
}

제거

리스트의 특정 위치에서 요소를 제거하려면 위치를 인자로 제공하며 removeAt() 함수를 사용해요. 제거되는 요소 뒤에 오는 모든 요소의 인덱스는 1씩 줄어듭니다.

fun main() {
//sampleStart
    val numbers = mutableListOf(1, 2, 3, 4, 3)    
    numbers.removeAt(1)
    println(numbers)
//sampleEnd
}

정렬

Collection Ordering에서는 컬렉션 요소를 특정 순서로 가져오는 연산을 설명해요. 가변 리스트를 위해 표준 라이브러리는 동일한 정렬 연산을 제자리(in place)에서 수행하는 비슷한 확장 함수를 제공합니다. 리스트 인스턴스에 그런 연산을 적용하면 정확히 그 인스턴스에서 요소의 순서가 바뀌어요.

제자리 정렬 함수는 읽기 전용 리스트에 적용하는 함수와 이름이 비슷하지만, ed/d 접미사가 없어요.

가변 리스트에서 호출된 asReversed()는 원본 리스트의 뒤집힌 뷰인 또 다른 가변 리스트를 반환해요. 그 뷰의 변경 사항은 원본 리스트에 반영됩니다. 다음 예시는 가변 리스트의 정렬 함수를 보여줘요.

fun main() {
//sampleStart
    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")

    numbers.shuffle()
    println("Shuffle: $numbers")

    numbers.reverse()
    println("Reverse: $numbers")
//sampleEnd
}

더 알아보기 (Learn more)