在前面的章節裡,我們討論了如何建立 Collection,以及操作它們的各種方式。Collection 裡的四大類別雖然看似相似,但實則有些微的差異。在這個章節裡,我們就要來討論 Collection 間的差異,以及如何依需求轉型。
首先用 Kotlin 官網上的 Collection 繼承關係圖了解一下 Collection 這個類別是怎麼被實作出來的,以及各自間的關係。
由這張圖可以看出 Collection<T>
是繼承 Iterable<T>
來的,它實作了讓我們可以迭代的行為,也就是為何我們可以用 for 把 Collection 內容倒出來的原因。而 Collection<T>
可謂萬物之始,這個 Interface 實作了許多常見的唯讀行為,像是取得 Collection 大小、檢查內容元素…等。我們在設計參數時,可以利用這個特性讓傳入的變數可以是 List
或 Set
。而 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 就會視為相同。乍看之下,List
跟 Array
一樣,但兩者間最大的不同是,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 的特性,但變數已經宣告、類別已經決定了怎麼辦?沒關係,Kotlin 也提供了一系列 to
開頭的轉換 method,讓我們可以在 List 與 Set 間轉換。當然,也可以將 List
或 Set
轉成 MutableList
或 MutableSet
。
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]}