iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
Software Development

新手也能懂的 Kotlin Collection 賞玩門道系列 第 22

第二十二天:Collection 差異及相互轉型

在前面的章節裡,我們討論了如何建立 Collection,以及操作它們的各種方式。Collection 裡的四大類別雖然看似相似,但實則有些微的差異。在這個章節裡,我們就要來討論 Collection 間的差異,以及如何依需求轉型。

Collection 的繼承關係

首先用 Kotlin 官網上的 Collection 繼承關係圖了解一下 Collection 這個類別是怎麼被實作出來的,以及各自間的關係。

Collection 繼承關係圖

由這張圖可以看出 Collection<T> 是繼承 Iterable<T> 來的,它實作了讓我們可以迭代的行為,也就是為何我們可以用 for 把 Collection 內容倒出來的原因。而 Collection<T> 可謂萬物之始,這個 Interface 實作了許多常見的唯讀行為,像是取得 Collection 大小、檢查內容元素…等。我們在設計參數時,可以利用這個特性讓傳入的變數可以是 ListSet。而 MutableCollection 則是讓 Collection 擁有變更的能力。

fun printAll(strings: Collection<String>) {
    for(s in strings) print("$s ")
    println()
}

fun main() {
    val stringList = listOf("one", "two", "one")
    printAll(stringList) // one two one 
    
    val stringSet = setOf("one", "two", "three")
    printAll(stringSet) // one two three 
}

List<T> 在儲存元素時會以 index 標記儲存的順序,index 是從 0 開始計算,儲存在 List 裡的元素是可以重複的,元素也可以包括 null。當我們比較兩個 List 的時候,只要 List 大小相同、結構一樣、內容物的順序也一樣的話,Kotlin 就會視為相同。乍看之下,ListArray 一樣,但兩者間最大的不同是,Array 的大小是在初始時就決定且不可變的,而 List 在實作時其實是 ArrayList,可以想像成是一種可變的 Array。而 MutableList<T> 則是讓 List<T> 擁有變更的能力。

val bob = Person("Bob", 31)
val people = listOf(Person("Adam", 20), bob, bob)
val people2 = listOf(Person("Adam", 20), Person("Bob", 31), bob)
println(people == people2) // true
bob.age = 32
println(people == people2) // false

Set<T> 則是只能儲存不重複的元素,即便是 null 也只能放一個,而元素在其中的順序是未定義的。當我們比較兩個 Set 的時候,只要大小相同、裡面放的元素相同,就算放進去的順序不同,兩個 Set 也會被視為相同是它的特色。MutableSet 是實作 MutableCollection 的版本,擁有變更的能力。

val numbers = setOf(1, 2, 3, 4)
println("Number of elements: ${numbers.size}") // Number of elements: 4

if (numbers.contains(1)) println("1 is in the set") // 1 is in the set

val numbersBackwards = setOf(4, 3, 2, 1)
println("The sets are equal: ${numbers == numbersBackwards}") // The sets are equal: true

從圖上可以看出來,Map<K, V> 並不是從 Collection 繼承出來的,不過它依然是一種 Collection 類別。Map 是儲存 key 與 value 的對照,key 是唯一的但 value 卻可以重複。當我們比較兩個 Map 的時候,只要大小相同、key 和 value 的組合都一樣,即便順序不同 Kotlin 也視為相同。

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)    
val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

println("The maps are equal: ${numbersMap == anotherMap}") // The maps are equal: true

比較表

在了解各個類別的繼承關係及特性後,是不是覺得很容易搞混呢?這邊將 Collection 之間的差異做成一個比較表:

類別 是否有序 是否唯一 儲存 是否支援解構
List 元素
Set 元素
Map 鍵值對

轉換 Collection

有時在資料操作的過程,會需要不同 Collection 的特性,但變數已經宣告、類別已經決定了怎麼辦?沒關係,Kotlin 也提供了一系列 to 開頭的轉換 method,讓我們可以在 List 與 Set 間轉換。當然,也可以將 ListSet 轉成 MutableListMutableSet

val myList = listOf(1, 2, 3, 4, 4)
myList.toList()
myList.toMutableList()
myList.toSet()
myList.toMutableSet()

既然 List 與 Set 可以互轉,聰明的你是否想到可以利用 Set 不可重複的特性,將 List 轉成 Set 就可以去除重複呢?的確,在前面的章節提到的 distinct(),其原理就是這樣實作的喔!

val name = listOf("Tom", "John", "Tina", "Sean", "John")
name.toSet().toList() // ["Tom", "John", "Tina", "Sean"]
name.distinct() // ["Tom", "John", "Tina", "Sean"]

Map 在資料結構上不同,所以在轉換上是有條件的。假如 List 或 Set 裡面放的是 Pair 的話,才有辦法直接轉成 Map

val myPairList = listOf(
    Pair(1, "one"),
    Pair(2, "two"),
    Pair(3, "three"),
)
myPairList.toMap()

假如不是 Pair 的話,我們也可以用轉換章節裡提過的 map() 先做一次轉換成 Pair,然後再轉成 Map 即可。

val myList = listOf(1, 2, 3, 4, 4)
val mySet = setOf("one", "two", "three", "four")
myList.zip(mySet).map {
    it.first to it.second
}.toMap() // {1=one, 2=two, 3=three, 4=four}

另外一種方式是透過 associate 開頭的 method,可以將 List 裡的內容取出後做關聯,直接組合出指定的 Map

data class User(val name: String, val age: Int, val hobbies: List<String>)
val user1 = User("John", 18, listOf("Hiking"))
val user2 = User("Sara", 25, listOf("Chess"))
val user3 = User("Dave", 34, listOf("Games"))
al myList = listOf(user1, user2, user3)
myList.associateBy({ it.name }, { it.hobbies }) // {John=[Hiking], Sara=[Chess], Dave=[Games]} 

參考資料


上一篇
第二十一天:Collection 操作之聚合
下一篇
第二十三天:深入 Collection 核心 - Range 與 Progression
系列文
新手也能懂的 Kotlin Collection 賞玩門道31

尚未有邦友留言

立即登入留言