在處理資料的時候,我們常常會先把 Collection 裡的資料取出後,再逐一轉換成另外一個類別或格式,由於 Collection 有 Iterator 的特點,所以比較阿 Q 的作法就是用 2 個 for 迴圈完成。但其實 Kotlin 的標準函式庫裡內建了一系列的轉換 Extension 可以用,也就是說,這些 Extension 可以幫你將現有的 Collection 以傳入的 Lambda 做轉換,效率++!
map()
讓我們可以把 Collection 裡面的元素一個一個拿出來,然後套用一個 Lambda 讓它轉換成其他東西。就行為來說,map()
跟 forEach()
都會逐一碰過元素一遍,不過差異就在 map()
會把轉換的過程回傳而 forEach()
不會。另外一個特點就是,map()
回傳的 Collection 一定會和原本的 Collection 一樣大。
val numbers = listOf(1, 2, 3)
numbers.map { it * it } // [1, 4, 9]
假如取出元素時,想要一併取出 index 做處理,標準函式庫裡也有 mapIndexed()
可以使用,讓操作更彈性一些。
val numbers = listOf(1, 2, 3)
numbers.mapIndexed { index, value ->
"$index: $value"
}
// [0: 1, 1: 2, 2: 3]
假如 Lambda 操作結果有可能是 null 的話,我們可以用 mapNotNull()
或 mapIndexedNotNull()
來過濾,就不需自行再用 filterNotNull()
額外處理。
val listOfNumbers = listOf(1, 2, 3)
listOfNumbers.mapNotNull {
if ( it == 2) null else it * 3
} // [3, 9]
listOfNumbers.mapIndexedNotNull { index, value ->
if (index == 0) null else value * index
} // [2, 6]
當我們需要把兩個 Collection 的元素做成對照表時,可以用 zip()
,它會將兩個 Collection 裡相同位置的元素做成 Pair
並回傳 List
。你可以想像就是把兩個 Collection 像是用拉鏈一樣把兩邊黏起來一樣。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
colors.zip(animals) // [(red, fox), (brown, bear), (grey, wolf)]
大多情況我們都會在兩個一樣大的 Collection 上套用 zip()
做 Pair。假如兩個 Collection 不一樣大的話,則會以小的為基準,多餘的都會被捨棄。
val animals = listOf("fox", "bear", "wolf")
val twoAnimals = listOf("fox", "bear")
colors.zip(twoAnimals) // [(red, fox), (brown, bear)]
twoAnimals.zip(colors) // [(fox, red), (bear, brown)]
有趣的是,zip()
有實作 infix function,所以你也可以把 zip 像關鍵字一樣地使用。
colors zip animals // [(red, fox), (brown, bear), (grey, wolf)]
zip()
還可以傳入 Lambda 做第二個參數,讓我們可以取得合併完後的每一個 Pair 內容做二次處理,這樣的彈性讓我們可以再少一步。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
// 傳入 Lambda 做第二個參數,取得 Pair 裡的 color 和 animal
colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]
// 同樣的結果用 `zip()` 和 `map()` 達成
colors.zip(animals).map {
"The ${it.second.capitalize()} is ${it.first}"
}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]
既然可以把兩個 Collection 黏在一起,當然就可以反過來拆開。假如你的 Collection 裡放了許多 Pair
,就可以用 unzip()
把 key 和 value 各自拆成 List,然後再以 Pair 回傳。
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
numberPairs.unzip() // ([one, two, three, four], [1, 2, 3, 4])
假如我們想要將 Collection 的元素與處理過的結果做對照,,可以用 associate
開頭的 method。它們可以將原本 Collection 的 key 或 value 跟 Lambda 處理過的結果做成 Map
。
associateWith()
會將原本 Collection 裡的元素當成 key,把 Lambda 處理過的結果當成 value 組合成 Map。不過要注意的是,associateWith()
會把重覆的元素去掉,回傳的 Map 裡只留下最後一個不重覆的元素。
val numbers = listOf("one", "two", "two", "three", "four")
numbers.associateWith { it.length } // {one=3, two=3, three=5, four=4}
假如想要反過來,把原本 Collection 裡的元素當成 value,那就改用 associateBy()
。或是你可以傳入 2 個 Lambda,keySelector
處理 key、valueTransform
處理 value。
val numbers = listOf("one", "two", "three", "four")
numbers.associateBy { it.first().toUpperCase() } // {O=one, T=three, F=four}
numbers.associateBy(
keySelector = { it.first().toUpperCase() },
valueTransform = { it.length }
) // {O=3, T=5, F=4}
associate()
的作法則是將 Collection 的元素傳給 Lambda,Lambda 則要回傳一個 Pair
,最後會將這成群的 Pair
轉成 Map
回傳。
val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
names.associate {
it.split(" ").let { (firstName, lastName) -> lastName to firstName }
}
// {Adams=Alice, Brown=Brian, Campbell=Clara}
假如你的 Collection 是巢狀的(比方說 List 裡面裝了數個 List),想要把 Collection 的階層打平,並取回各階層的元素成一個單層的 List 的話,flatten()
就是你要找的!
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
numberSets.flatten() // [1, 2, 3, 4, 5, 6, 1, 2]
flatMap()
則提供更彈性的巢狀 Collection 處理機制,它將傳入的 Lambda 套用在元素上以產生另一個 Collection,最後會再把階層打平回傳一個只有一層的 List。行為有點類似綜合運用了 flatten()
及 map()
。
data class StringContainer(val values: List<String>)
val containers = listOf(
StringContainer(listOf("one", "two", "three")),
StringContainer(listOf("four", "five", "six")),
StringContainer(listOf("seven", "eight"))
)
containers.flatMap { it.values } // [one, two, three, four, five, six, seven, eight]
若需要將 Collection 的內容印出來方便閱讀,可以用 joinToString()
來合併成單一 String。可以注意到,joinToString()
跟 toString()
有點像但輸出的格式不同。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString() // one, two, three, four
numbers.toString() // [one, two, three, four]
若是要調整輸出的格式,joinToString()
可以傳入 separator
、prefix
、postfix
等參數,在輸出的時候,會先輸出 prefix
,接著輸出每個元素並以 separator
隔開,最後再以 postfix
結束。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end") // start: one | two | three | four: end
假如 Collection 的內容很大,我們可能只需要印出前面幾個代表性的元素,後面就用「…」顯示。則可以 limit
設定輸出的個數、truncated
設定超過個數時要用什麼字串做結尾。因為 limit
和 truncated
是 joinToString()
的第 4-5 個參數,若前面的參數都不設定的話,記得要用顯示指定參數名稱。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString(limit = 2, truncated = "...") // one, two, ...
joinToString()
甚至還有第 6 個參數,你可以傳入一個 Lambda 決定要怎麼轉換成字串。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString { "Element: ${it.toUpperCase()}"} // Element: ONE, Element: TWO, Element: THREE, Element: FOUR
假如是要把轉換好的字串加到其他字串裡的話,則要改用 joinTo()
。要傳入的參數記得要是一個 Appendable
物件。
val numbers = listOf("one", "two", "three", "four")
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString) // The list of numbers: one, two, three, four
在這個章節裡,我們實驗了許多 Collection 轉換的方式。熟悉這些 method 後,對於用 Kotlin 處理資料將會更加上手。許多 Kotlin 高手之所以程式碼可以如此簡潔,也是因為能將這些技巧發揮到極致的關係。為了一覽這些 API 在不同 Collection 上的行為,以下用表格來整理本章所討論到的 method:
行為 | Array | List | Set | Map | |
---|---|---|---|---|---|
map() | 轉換元素 | v | v | v | v |
mapIndexed() | 轉換元素及索引 | v | v | v | v |
mapNotNull() | 轉換元素後排除 null | v | v | v | v |
mapIndexedNotNull() | 轉換元素及索引後排除 null | v | v | v | v |
zip() | 將兩個集合相黏 | v | v | v | v |
unzip() | 將 Pair 拆開 | v | v | v | v |
flatten() | 將集合平面化 | v | v | v | v |
flatMap() | 依 Lambda 把集合平面化 | v | v | v | v |
joinToString() | 把元素合併成字串 | v | v | v | v |
joinTo() | 把元素整併到別的字串 | v | v | v | v |