Преобразование коллекций
Стандартная библиотека 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
}