컬렉션 변환 연산

컬렉션 변환 연산 (Collection transformation operations)

Kotlin 표준 라이브러리는 컬렉션 변환을 위한 확장 함수들을 제공합니다. 이 함수들은 주어진 변환 규칙에 따라 기존 컬렉션으로부터 새 컬렉션을 만듭니다. 이 페이지에서는 사용 가능한 컬렉션 변환 함수를 개괄적으로 살펴볼게요.

출처: Kotlin 공식 문서

본문

Map (매핑)

매핑 변환은 한 컬렉션의 요소들에 함수를 적용한 결과로 새 컬렉션을 만듭니다. 기본 매핑 함수는 map()이에요. 주어진 람다 함수를 각 요소에 차례로 적용하고, 그 람다 결과들의 리스트를 반환합니다. 결과의 순서는 원래 요소 순서와 같아요. 추가로 요소 인덱스를 인자로 사용해야 하는 변환이라면 mapIndexed()를 사용해요.

fun main() {
//sampleStart
    val numbers = setOf(1, 2, 3)
    println(numbers.map { it * 3 })
    println(numbers.mapIndexed { idx, value -> value * idx })
//sampleEnd
}

변환이 어떤 요소에서 null을 만들면, map() 대신 mapNotNull()을, mapIndexed() 대신 mapIndexedNotNull()을 호출해서 결과 컬렉션에서 null을 걸러낼 수 있어요.

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

맵을 변환할 때는 두 가지 선택지가 있어요. 키를 변환하고 값은 그대로 두거나, 그 반대죠. 키에 주어진 변환을 적용하려면 mapKeys()를, 값은 mapValues()로 변환합니다. 두 함수 모두 맵 엔트리를 인자로 받는 변환을 사용하므로, 키와 값 둘 다를 다룰 수 있어요.

fun main() {
//sampleStart
    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    println(numbersMap.mapKeys { it.key.uppercase() })
    println(numbersMap.mapValues { it.value + it.key.length })
//sampleEnd
}

Zip (지핑)

지핑(zip) 변환은 두 컬렉션에서 같은 위치에 있는 요소들로 쌍(pair)을 만듭니다. Kotlin 표준 라이브러리에서는 zip() 확장 함수로 처리해요.

컬렉션이나 배열에 다른 컬렉션(또는 배열)을 인자로 전달해 zip()을 호출하면, Pair 객체들의 List를 반환합니다. 수신자(receiver) 컬렉션의 요소들이 이 쌍들의 첫 번째 요소가 되죠.

두 컬렉션의 크기가 다르면, zip()의 결과 크기는 더 작은 쪽을 따릅니다. 더 큰 컬렉션의 마지막 요소들은 결과에 포함되지 않아요.

zip()은 중위(infix) 형식인 a zip b로도 호출할 수 있습니다.

fun main() {
//sampleStart
    val colors = listOf("red", "brown", "grey")
    val animals = listOf("fox", "bear", "wolf")
    println(colors zip animals)

    val twoAnimals = listOf("fox", "bear")
    println(colors.zip(twoAnimals))
//sampleEnd
}

zip()은 두 파라미터(수신자 요소와 인자 요소)를 받는 변환 함수와 함께 호출할 수도 있어요. 이 경우 결과 List는 같은 위치의 수신자·인자 요소 쌍에 대해 변환 함수가 반환한 값들을 담습니다.

fun main() {
//sampleStart
    val colors = listOf("red", "brown", "grey")
    val animals = listOf("fox", "bear", "wolf")

    println(colors.zip(animals) { color, animal -> "The ${animal.replaceFirstChar { it.uppercase() }} is $color"})
//sampleEnd
}

Pair들의 List가 있다면 역변환 — 언지핑(unzipping) — 을 할 수 있는데, 이 쌍들로부터 두 리스트를 만듭니다.

  • 첫 번째 리스트는 원래 리스트의 각 Pair의 첫 번째 요소를 담아요.
  • 두 번째 리스트는 두 번째 요소를 담아요.

쌍의 리스트를 언지핑하려면 unzip()을 호출하면 됩니다.

fun main() {
//sampleStart
    val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
    println(numberPairs.unzip())
//sampleEnd
}

Associate (연관 짓기)

연관(association) 변환은 컬렉션 요소와 그것에 연관된 특정 값들로 맵을 만듭니다. 연관 유형에 따라 요소는 맵의 키나 값이 될 수 있어요.

기본 연관 함수인 associateWith()는 원래 컬렉션의 요소가 키이고, 값은 주어진 변환 함수로 그것들로부터 만들어지는 Map을 만듭니다. 두 요소가 같으면 맵에는 마지막 것만 남아요.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.associateWith { it.length })
//sampleEnd
}

컬렉션 요소를 값으로 하는 맵을 만들려면 associateBy() 함수가 있어요. 이 함수는 요소 값에 기반해 키를 반환하는 함수를 받습니다. 두 요소의 키가 같으면 맵에는 마지막 것만 남아요.

associateBy()는 값 변환 함수와 함께 호출할 수도 있습니다.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")

    println(numbers.associateBy { it.first().uppercaseChar() })
    println(numbers.associateBy(keySelector = { it.first().uppercaseChar() }, valueTransform = { it.length }))
//sampleEnd
}

키와 값이 모두 컬렉션 요소로부터 어떻게든 만들어진 맵을 구성하는 또 다른 방법은 associate() 함수예요. 이 함수는 해당 맵 엔트리의 키와 값을 담은 Pair를 반환하는 람다 함수를 받습니다.

associate()는 수명이 짧은 Pair 객체를 만들어 성능에 영향을 줄 수 있다는 점에 유의하세요. 그래서 associate()는 성능이 중요하지 않거나 다른 선택지보다 더 나을 때 사용해야 합니다. 후자의 예로는 키와 그에 해당하는 값이 요소로부터 함께 만들어지는 경우가 있어요.

fun main() {
data class FullName (val firstName: String, val lastName: String)

fun parseFullName(fullName: String): FullName {
    val nameParts = fullName.split(" ")
    if (nameParts.size == 2) {
        return FullName(nameParts[0], nameParts[1])
    } else throw Exception("Wrong name format")
}

//sampleStart
    val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
    println(names.associate { name -> parseFullName(name).let { it.lastName to it.firstName } })
//sampleEnd
}

여기서는 먼저 요소에 변환 함수를 호출한 뒤, 그 함수 결과의 속성들로 쌍을 만들고 있어요.

Flatten (펼치기)

중첩된 컬렉션을 다룬다면, 중첩 컬렉션 요소에 평면적으로 접근하게 해주는 표준 라이브러리 함수들이 유용할 거예요.

첫 번째 함수는 flatten()입니다. 컬렉션의 컬렉션, 예를 들어 Set들의 List에 호출할 수 있어요. 이 함수는 중첩 컬렉션들의 모든 요소를 담은 단일 List를 반환합니다.

fun main() {
//sampleStart
    val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
    println(numberSets.flatten())
//sampleEnd
}

또 다른 함수인 flatMap()은 중첩 컬렉션을 처리하는 유연한 방법을 제공합니다. 컬렉션 요소를 다른 컬렉션으로 매핑하는 함수를 받죠. 결과적으로 flatMap()은 모든 요소에 대한 반환 값들의 단일 리스트를 반환합니다. 그래서 flatMap()map()(매핑 결과가 컬렉션인)과 flatten()을 연이어 호출한 것처럼 동작해요.

data class StringContainer(val values: List<String>)

fun main() {
//sampleStart
    val containers = listOf(
        StringContainer(listOf("one", "two", "three")),
        StringContainer(listOf("four", "five", "six")),
        StringContainer(listOf("seven", "eight"))
    )
    println(containers.flatMap { it.values })
//sampleEnd
}

문자열 표현

컬렉션 내용을 읽을 수 있는 형식으로 가져와야 한다면, 컬렉션을 문자열로 변환하는 함수 — joinToString()joinTo() — 를 사용하세요.

joinToString()은 제공된 인자들에 기반해 컬렉션 요소들로 단일 String을 만듭니다. joinTo()는 같은 일을 하지만 결과를 주어진 Appendable 객체에 덧붙여요.

파라미터 기본값으로 호출하면, 이 함수들은 컬렉션에서 toString()을 호출한 것과 비슷한 결과를 반환합니다 — 요소들의 문자열 표현을 쉼표와 공백으로 구분한 String이요.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")

    println(numbers)
    println(numbers.joinToString())

    val listString = StringBuffer("The list of numbers: ")
    numbers.joinTo(listString)
    println(listString)
//sampleEnd
}

커스텀 문자열 표현을 만들려면 함수 인자 separator, prefix, postfix로 매개변수를 지정할 수 있어요. 결과 문자열은 prefix로 시작해 postfix로 끝나고, separator는 마지막 요소를 제외한 각 요소 뒤에 옵니다.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end"))
//sampleEnd
}

더 큰 컬렉션에서는 결과에 포함될 요소 수인 limit를 지정하고 싶을 수 있어요. 컬렉션 크기가 limit를 초과하면, 나머지 요소들은 모두 truncated 인자의 값 하나로 대체됩니다.

fun main() {
//sampleStart
    val numbers = (1..100).toList()
    println(numbers.joinToString(limit = 10, truncated = "<...>"))
//sampleEnd
}

마지막으로 요소 자체의 표현을 커스터마이즈하려면 transform 함수를 제공하면 됩니다.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.joinToString { "Element: ${it.uppercase()}"})
//sampleEnd
}

더 알아보기 (Learn more)