Преобразование коллекций

Стандартная библиотека Kotlin предоставляет целый набор функций-расширений для преобразования коллекций. Они создают новые коллекции из существующих на основе предоставляемых правил преобразования. В этом разделе находится общий обзор подобных функций.

Map

Группа функций, корнем которых является map(). Она позволяет преобразовать исходную коллекцию путём применения заданной лямбда-функции к каждому её элементу, объединяя результаты в новую коллекцию. При этом порядок элементов сохраняется. Этот процесс преобразования также называют маппингом (ориг. mapping). Если для преобразования коллекции вам требуется знать индекс элементов, то используйте функцию mapIndexed().

fun main() {
    val numbers = setOf(1, 2, 3)
    println(numbers.map { it * 3 }) // [3, 6, 9]
    println(numbers.mapIndexed { idx, value -> value * idx }) // [0, 2, 6]
}

Если какой-либо элемент или элементы могут быть преобразованы в значение равное null, то вместо map() можно использовать функцию mapNotNull(), которая отфильтрует такие элементы и они не попадут в новую коллекцию. Соответственно, вместо mapIndexed() можно использовать mapIndexedNotNull().

fun main() {
    val numbers = setOf(1, 2, 3)
    println(numbers.mapNotNull {
      if ( it == 2) null else it * 3
    }) // [3, 9]
    println(numbers.mapIndexedNotNull { idx, value ->
      if (idx == 0) null else value * idx
    }) // [2, 6]
}

Ассоциативные списки можно преобразовывать двумя способами: преобразовывать ключи, не изменяя значения, и наоборот. Для преобразования ключей используйте функцию mapKeys(); в свою очередь mapValues() преобразует значения. Они обе используют функцию преобразования, которая в качестве аргумента принимает пару “ключ-значение”, поэтому вы можете работать как с ключом, так и со значением.

fun main() {
    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    println(numbersMap.mapKeys { it.key.uppercase() }) // {KEY1=1, KEY2=2, KEY3=3, KEY11=11}
    println(numbersMap.mapValues { it.value + it.key.length }) // {key1=5, key2=6, key3=7, key11=16}
}

Zip

Данная группа функций преобразования - zipping - берёт два списка и создаёт из их элементов пары. При этом пары создаются из элементов с одинаковыми индексами. В стандартной библиотеке Kotlin для такого преобразования есть функция-расширение zip().

Если функцию zip() вызвать для одной коллекции и при этом передать ей в качестве аргумента другую коллекцию, то в результате вы получите список типа List<Pair<K, V>>. Элементы коллекции-получателя будут первыми элементами в этих объектах Pair.

Если коллекции имеют разные размеры, то zip() вернёт новую коллекцию, длина которой равняется минимальной из исходных коллекций; последние элементы бОльшей коллекции будут отсечены.

Также zip() можно вызывать в инфиксной форме a zip b.

fun main() {
    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)]
}

Вместе с коллекцией zip() можно передать функцию преобразования, которая принимает два параметра: элемент коллекции-получателя и элемент коллекции-аргумента, при этом оба элемента располагаются в своих коллекциях по одинаковому индексу. В этом случае вернётся List с содержимым из результатов вычисления функции преобразования.

fun main() {
    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"
    }) // [The Fox is red, The Bear is brown, The Wolf is grey]
}

Для списка типа List<Pair<K, V>> можно выполнить обратное преобразование - unzipping, которое разбирает пары на два отдельных списка:

  • В первый список помещаются первые элементы каждого объекта Pair.
  • Во второй список помещаются вторые элементы каждого объекта Pair.

Чтобы “распаковать” такой список вызовите для него функцию unzip().

fun main() {
    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])
}

Associate

Функции преобразования данного типа позволяют создавать ассоциативные списки из элементов коллекции и связанных с ними значений. При этом элементы могут быть как ключами ассоциативного списка, так и значениями.

Основная функция здесь - associateWith(). Она создаёт Map, в которой элементы исходной коллекции являются ключами, а значения вычисляются с помощью функции преобразования. Если встречаются два равных элемента, то в ассоциативный список попадёт только последний из них.

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

Для создания ассоциативного списка, в котором элементы коллекции будут выступать в роли значений, существует функция associateBy(). Она принимает функцию, которая в свою очередь возвращает ключ, основанный на значении элемента. Если встречаются два равных элемента, то в ассоциативный список попадёт только последний из них.

Также associateBy() можно вызвать с функцией, преобразующей значения.

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

    println(numbers.associateBy { it.first().uppercaseChar() }) // {O=one, T=three, F=four}
    println(numbers.associateBy(
      keySelector = { it.first().uppercaseChar() },
      valueTransform = { it.length }
    )) // {O=3, T=5, F=4}
}

Есть еще один способ создания ассоциативного списка, при котором ключи и значения тем или иным образом создаются на основе элементов коллекции - при помощи функции 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")
    }

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

В примере выше сначала вызывается функция преобразования элемента, а затем происходит создание пары на основе полученного от этой функции результата.

Flatten

При работе с вложенными коллекциями вам могут пригодиться функции, обеспечивающие “плоский” доступ к их элементам.

Первая функция - flatten(). Её можно использовать для коллекции, содержащей другие коллекции, например, для List, в котором находятся Set-ы. Функция возвращает объединённый List, состоящий из всех элементов всех вложенных коллекций.

fun main() {
    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]
}

Другая функция - flatMap(), которая обеспечивает гибкий способ работы с вложенными коллекциями. Она принимает функцию, которая маппит элемент исходной коллекции в другую коллекцию. В качестве результата flatMap() возвращает объединённый список из всех обработанных элементов. По сути flatMap() ведёт себя как вызов map() (с возвращением коллекции в качестве результата маппинга) и flatten().

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

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

Преобразование коллекции в String

С помощью функций joinToString() и joinTo() содержимое коллекции можно преобразовать в читаемый вид - String.

joinToString() на основе предоставленных аргументов объединяет элементы коллекции в строку. joinTo() делает то же самое, но добавляет результат к переданному объекту Appendable.

При вызове этих функций с аргументами по умолчанию, они вернут результат, аналогичный вызову toString() для коллекции: элементы будут представлены в виде String, разделённые между собой запятой с пробелом.

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

    println(numbers) // [one, two, three, four]
    println(numbers.joinToString()) // one, two, three, four

    val listString = StringBuffer("The list of numbers: ")
    numbers.joinTo(listString)
    println(listString) // The list of numbers: one, two, three, four
}

Но также вы можете кастомизировать строковое представление коллекции, указав необходимые вам параметры при помощи специальных аргументов - separator, prefix, и postfix. Преобразованная строка будет начинаться с префикса (prefix) и заканчиваться постфиксом (postfix). А separator будет добавлен после каждого элемента, кроме последнего.

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

Если коллекция большая, то вы можете указать limit - количество элементов, которые будут включены в результат. При этом все элементы, превышающие limit, будут заменены одним значением аргумента truncated.

fun main() {
    val numbers = (1..100).toList()
    println(numbers.joinToString(
      limit = 10,
      truncated = "<...>")
    ) // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, <...>
}

Ну и наконец, чтобы кастомизировать представление самих элементов, передайте в качестве аргумента функцию transform.

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