如何使用 Kotlin 的集合轉換

如何使用 Kotlin 的集合轉換

Kotlin 為集合提供了一套轉換機制稱作 Collection Transformations,這些機制可以從一個已經存在的集合轉換成另外一個新集合。

說明

Mapping

Map() 轉換可以將一組集合,按照順序的進行轉換成另外一組新的集合,如果你想要取得索引值,則可以透過 mapIndexed() 進行轉換。

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

由上面可以看出來,我們將既有的集合透過每個元素乘上 3 變成了新的元素集合,另外一個操作是透過 mapIndexed 來將每個元素乘上索引值所得到的結果,如下所示。

[3, 6, 9]
[0, 2, 6]

如果集合中有 null 元素,你可以透過 filter 方法先進行過濾,或者透過 mapNotNull 或 mapIndexedNotNull 將其濾掉。

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 })

面對 map 的轉換你有兩種選擇:一種是轉換 key ,值不變或者轉換值,key 不變,你可以透過兩個方法來達成這個需求,mapKeys() mapValues()

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

對所有的 key 轉換成大寫以及對所有的值加上值的長度。

{KEY1=1, KEY2=2, KEY3=3, KEY11=11}
{key1=5, key2=6, key3=7, key11=16}

Zipping

你可以透過 zip 來轉換兩個陣列合併為同一個陣列。

val colors = listOf("red", "brown", "gray")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)

可以看到輸出會出現對應的結果。

[(red, fox), (brown, bear), (gray, wolf)]

如果合併的數量不對等的情況,會以數量少的為主。

val colors = listOf("red", "brown", "gray")
val twoAnimals = listOf("fox", "bear")
println(colors.zip(twoAnimals))

來看看輸出的結果。

[(red, fox),(brown, bear)]

在上面的例子,可以看到是透過 zip() 這個方法來實作,其實你也可以這樣玩。

val colors = listOf("red", "brown", "gray")
val animals = listOf("fox", "bear", "wolf")
val result = colors.zip(animals){color, animal ->
  "The $animal is $ color"
}
println(result)

讓我們來看一下結果。

[fox is red, bear is brown, wolf is gray]

除了 zip 以外,你也可以透過 unzip 來把 list of pairs 解開。

val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3)
println(numberPairs.unzip())

來看一下結果。

([one, two, three], [1, 2, 3])

Association

可以透過 associateWith 將 list 其轉換成 key 對應 value 的集合,如果有兩個元素相同值,那麼就只會設定成為同一組。

val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith{ it.length})

可以看到會根據字串的字元數字轉化成 map。

{one=3, two=3, three=5, four=4}

你可以透過 associateBy 來轉換成 map,同樣的遇到同樣字元只會轉換成一組,你也可以透過 keySelector 以及 valueTransform 來決定 key 跟 value 該組成什麼形狀。

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

println(numbers.associateBy { it.first().toUpperCase() })
println(numbers.associateBy(keySelector = { it.first().toUpperCase() }, valueTransform = { it.length }))

結果如下。

{O=one, T=three, F=four}
{O=3, T=5, F=4}

透過 it.lastName to it.firstName 的方式來組成新的 map。

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")
}

fun main(args: Array<String>) {
    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}

Flattening

如果你的集合裡面還有集合,那麼就可以用 flatten() 這個方法將其攤平。

val numberSets = listOf(listOf(1, 2, 3), listOf(4, 5, 6), listOf(1, 2))
println(numberSets.flatten())

你就會看到結果如下。

[1, 2, 3, 4, 5, 6, 1, 2]

或者你可以直接用 flatMap 來進行轉換,flatMap 的好處就是可以在 lambda 內進行一些操作。

val numberSets = listOf(listOf(1, 2, 3), listOf(4, 5, 6), listOf(1, 2))
println(numberSets.flatMap { it.map { value -> value * 10 } })

結果如下。

[10, 20, 30, 40, 50, 60, 10, 20]

String representation

如果你要插入一些字串可以透過 joinToString 來進行調整,其中 prefix 與 postfix 可以設定頭跟尾部要加入特定字串。

val numbers = listOf(1, 2, 3, 4)
println(names.joinToString(";","$","%"))

結果如下。

$1;2;3;4%

也可以透過參數設定決定要抓取多少數量,以及最後尾端要加入什麼字串。

val numbers = (1..100).toList()
println(numbers.joinToString(limit = 10, truncated = "..."))

結果如下。

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

你也可以透過 joinToString 指定每個元素要變成什麼模樣。

val numbers = listOf(1, 2, 3, 4)
println(numbers.joinToString { "Element: $it" })

結果如下。

Element: 1, Element: 2, Element: 3, Element: 4