在 Kotlin 中使用集合

1. 簡介

在許多應用程式中,可能都有以清單顯示的資料,例如聯絡人、設定、搜尋結果等。

46df844b170f4272.png

但在您目前編寫的程式碼中,大多是使用以單一值組成的資料,例如在畫面上顯示的數字或文字。如要建構資料量不固定的應用程式,您需要瞭解如何使用集合。

利用集合類型 (有時稱為資料結構),您就能井然有序地儲存多個值 (通常是資料類型相同的值)。集合可能是已排序的清單、一組不重複的值,或是兩種資料類型的值之間的對應關係。如果能有效運用集合,就可以實作 Android 應用程式的常見功能 (例如捲動清單),並解決與資料量不固定有關的各種實際程式設計問題。

本程式碼研究室討論如何在程式碼中使用多個值,並介紹多種不同的資料結構,包括陣列、清單、組合和對應。

必要條件

  • 熟悉 Kotlin 中的物件導向程式設計,包含類別、介面和泛型。

課程內容

  • 如何建立及修改陣列。
  • 如何使用 ListMutableList
  • 如何使用 SetMutableSet
  • 如何使用 MapMutableMap

軟硬體需求

  • 可存取 Kotlin Playground 的網路瀏覽器。

2. Kotlin 中的陣列

什麼是陣列?

如要將程式中任意數量的值分組,使用陣列是最簡單的做法。

就像一組太陽能板稱為太陽能陣列 (solar array)、學習 Kotlin 可獲得一系列 (an array of) 程式設計工作機會,Array 代表的就是多個值。具體而言,陣列是指一系列具有相同資料類型的值。

33986e4256650b8b.png

  • 陣列包含多個稱為「元素」的值,有時也稱為「項目」
  • 陣列中的元素都會排序,可透過索引存取。

什麼是索引?索引是與陣列元素相對應的整數。索引會指出項目與陣列中起始元素之間的距離,這稱為以零為開頭的索引。陣列中第一個元素的索引為 0,則第二個元素的索引為 1,因為它與第一個元素距離一個位置,以此類推。

bb77ec7506ac1a26.png

在裝置記憶體中,陣列中的元素會依序排列儲存。本程式碼研究室不會說明基礎細節,但上述資訊有以下兩項重要涵義:

  • 有了索引,就能快速存取陣列元素。您可以依索引隨機存取陣列的任一元素,所需時間也會與隨機存取其他任一元素相同,因此會說陣列具有「隨機存取」機制。
  • 陣列有固定大小。也就是說,加入陣列的元素數量無法超過此大小。若在大小為 100 個元素的陣列中嘗試存取索引 100 的元素,系統會擲回例外狀況,因為最高索引為 99 (提醒您,第一個索引是 0,不是 1)。不過,您可以修改陣列中位於索引的值。

如要在程式碼中宣告陣列,請使用 arrayOf() 函式。

69e283b32d35f799.png

arrayOf() 函式會將陣列元素視為參數,然後傳回與所傳入參數相符的陣列類型。由於 arrayOf() 的參數數量不盡相同,這或許會與您看到的其他函式略有不同。如果將兩個引數傳入 arrayOf(),則產生的陣列會包含兩個元素,索引分別是 0 和 1。如果傳入三個引數,則產生的陣列將有 3 個元素,索引是 0 到 2。

現在我們來瞭解陣列的實際運作情形,順便探索一下太陽系吧!

  1. 前往 Kotlin Playground
  2. main() 中建立 rockPlanets 變數。呼叫 arrayOf(),然後傳入類型 String 與四個字串,分別代表太陽系中的岩石行星。
val rockPlanets = arrayOf<String>("Mercury", "Venus", "Earth", "Mars")
  1. 由於 Kotlin 會使用類型推論,因此您可以在呼叫 arrayOf() 時省略類型名稱。在 rockPlanets 變數下方新增另一個變數 gasPlanets,而不將類型傳入角括號。
val gasPlanets = arrayOf("Jupiter", "Saturn", "Uranus", "Neptune")
  1. 您可以利用陣列執行一些有趣的操作。舉例來說,就像數字類型 IntDouble 一樣,您可以同時加入兩個陣列。請建立名為 solarSystem 的新變數,然後使用加號 (+) 運算子,將新變數設為等於 rockPlanetsgasPlanets 的結果。結果會是新的陣列包含 rockPlanets 陣列的所有元素,以及和 gasPlanets 陣列的元素。
val solarSystem = rockPlanets + gasPlanets
  1. 執行程式,確認能否正常運作。目前還不應顯示任何輸出內容。

存取陣列中的元素

您可以依索引存取陣列的元素。

1f8398eaee30c7b0.png

這就是下標語法。其中包含三個部分:

  • 陣列名稱。
  • 左方括號 ([) 和右方括號 (])。
  • 位於方括號中的陣列元素索引。

接下來要利用索引存取 solarSystem 陣列的元素。

  1. main() 中,存取並顯示 solarSystem 陣列的每個元素。請注意,第一個索引是 0,最後一個索引是 7
println(solarSystem[0])
println(solarSystem[1])
println(solarSystem[2])
println(solarSystem[3])
println(solarSystem[4])
println(solarSystem[5])
println(solarSystem[6])
println(solarSystem[7])
  1. 執行程式。元素的順序與您呼叫 arrayOf() 時的順序相同。
Mercury
Venus
Earth
Mars
Jupiter
Saturn
Uranus
Neptune

您也可以透過索引設定陣列元素的值。

9469e321ed79c074.png

存取索引的方法與先前相同:先是陣列名稱,然後是包含索引的左方括號和右方括號。然後是指派運算子 (=) 和一個新的值。

現在來練習修改 solarSystem 陣列的值。

  1. 我們可以為火星重新命名,供未來的人類移民使用。請存取索引 3 的元素,然後設為 "Little Earth"
solarSystem[3] = "Little Earth"
  1. 顯示索引 3 的元素。
println(solarSystem[3])
  1. 執行程式。陣列的第四個元素 (位於索引 3) 已更新。
...
Little Earth
  1. 現在,假設科學家發現海王星之後的第九顆行星,稱為「冥王星」。我們先前提過,陣列的大小無法調整。如果嘗試調整會怎麼樣?我們來嘗試將冥王星新增至 solarSystem 陣列。新增冥王星至索引 8,因為這是陣列中的第 9 個元素。
solarSystem[8] = "Pluto"
  1. 執行程式碼。系統會擲回 ArrayIndexOutOfBounds 例外狀況。由於陣列已有 8 個元素,因此如同預期,您無法直接加入第 9 個元素。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
  1. 移除在陣列中加入的冥王星。

要移除的程式碼

solarSystem[8] = "Pluto"
  1. 如要讓陣列大小大於現有陣列,您需要建立新陣列。定義名為 newSolarSystem 的新變數,如下所示。此陣列可儲存九個元素,而非八個。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
  1. 現在嘗試顯示索引 8 的元素。
println(newSolarSystem[8])
  1. 執行程式碼,然後就會發現程式碼執行時沒有任何例外狀況。
...
Pluto

做得好!現在您對陣列已有所瞭解,可以利用集合執行幾乎所有操作。

等等,不是馬上開始!雖然陣列是程式設計的基本面向之一,但如果工作需要新增和移除元素、確保集合內容不重複或將物件對應至其他物件,使用陣列並不是簡單直接的做法,而且應用程式程式碼會很快變成一團亂。

因此,大部分的程式設計語言 (包括 Kotlin) 都採用特殊的集合類型,處理實際應用程式中常見的情況。以下各節說明三種常用的集合:ListSetMap。此外,您也會瞭解常見的屬性和方法,以及這些集合類型的使用情境。

3. 清單

清單是經過排序且可調整大小的集合,通常是實作為可調整大小的陣列。陣列填滿時,如果嘗試插入新元素,系統會將陣列複製到較大的新陣列。

a4970d42cd1d2b66.png

有了清單,您也可以在其他元素之間,於特定索引插入新元素。

a678d6a41e6afd46.png

這就是能在清單中新增與移除元素的方式。大多數情況下,無論清單元素的數量為何,在清單中加入任何元素所需的時間都一樣。每隔一段時間,如果加入新元素會導致陣列超過定義的大小,就可能必須移動陣列元素,才能為新元素騰出空間。清單會自動完成上述所有操作,但實際上只是系統在需要時將陣列換新而已。

ListMutableList

您將在 Kotlin 中看到的集合類型會實作一或多個介面。如本單元先前的「泛型、物件和擴充功能程式碼研究室所述,介面提供一組標準的屬性和方法,以供類別進行實作。實作 List 介面的類別可提供實作內容給 List 介面的所有屬性和方法。MutableList 也是如此。

那麼 ListMutableList 有哪些功能?

  • List 這種介面可定義與項目的唯讀排序集合相關的屬性和方法。
  • MutableList 會定義修改清單的方法 (例如新增及移除元素),即可擴充 List 介面。

這些介面僅指定 List 和/或 MutableList 的屬性與方法。每個屬性和方法的實作方法,是由擴充介面的類別決定。如果不是每次都使用,上述以陣列為基礎的實作方法也仍會是您最常用的實作方式,但 Kotlin 也允許其他類別擴充 ListMutableList

listOf() 函式

arrayOf() 一樣,listOf() 函式會以項目做為參數,但會傳回 List,而非陣列。

  1. main() 中移除現有程式碼。
  2. main() 中呼叫 listOf(),建立名為 solarSystem 的行星 List
fun main() {
    val solarSystem = listOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
}
  1. List 具有 size 屬性,可取得清單中的元素數量。顯示 solarSystem 清單的 size
println(solarSystem.size)
  1. 執行程式碼。清單大小應為 8。
8

從清單中存取元素

就像陣列一樣,您可以使用下標語法,從 List 存取特定索引的元素。您也可以使用 get() 方法執行相同操作。下標語法和 get() 方法都會使用 Int 做為參數,並傳回該索引的元素。和 Array 一樣,ArrayList 是以零為開頭的索引,因此第四個元素會位於索引 3

  1. 使用下標語法顯示位於索引 2 的行星。
println(solarSystem[2])
  1. 呼叫 solarSystem 清單中的 get(),顯示位於索引 3 的元素。
println(solarSystem.get(3))
  1. 執行程式碼。位於索引 2 的元素為 "Earth",位於索引 3 的元素則為 "Mars"
...
Earth
Mars

除了依索引取得元素以外,您也可以使用 indexOf() 方法搜尋特定元素的索引。indexOf() 方法會搜尋清單的特定元素 (以引數形式傳入),然後傳回該元素第一次出現的索引。如果清單中沒有出現該元素,則會傳回 -1

  1. 顯示 solarSystem 中呼叫 indexOf() 傳入 "Earth" 的結果。
println(solarSystem.indexOf("Earth"))
  1. 呼叫傳入 "Pluto"indexOf(),然後顯示結果。
println(solarSystem.indexOf("Pluto"))
  1. 執行程式碼。一個元素與 "Earth" 相符,因此顯示索引 2。沒有與 "Pluto" 相符的元素,因此顯示 -1
...
2
-1

使用 for 迴圈疊代清單元素

瞭解函式類型和 lambda 運算式後,就能利用 repeat() 函式多次執行程式碼。

程式設計的常見工作,是對清單中的每個元素執行一次工作。Kotlin 包含一項稱為 for 迴圈的功能,可透過簡潔清楚的語法完成這項作業。這通常稱為透過清單執行「迴圈」,或是「疊代」清單

f11277e6af4459bb.png

如要透過清單執行迴圈,請使用 for 關鍵字,並在後面加上一對左右括號。在括號中加入變數名稱,後方依序加上 in 關鍵字和集合名稱。右括號之後是一對大括弧,您可以在大括弧中加入要針對集合中每個元素執行的程式碼。這就是迴圈「本體」。每次執行這個程式碼,就稱為「疊代」

in 關鍵字之前的變數不會以 valvar 宣告,而是假設為 get-only。命名方式不限。如果清單的名稱為複數 (例如 planets),則變數通常會以單數格式命名 (例如 planet)。將變數命名為 itemelement 也很常見。

這會用做與集合中目前元素相對應的暫時變數,也就是索引 0 的元素適用於第一次疊代、索引 1 的元素適用於第二次疊代,以此類推,並且可在大括弧中存取。

如要查看此操作,請使用 for 迴圈顯示出各個行星名稱,且每行單獨顯示一個名稱。

  1. main() 中最近呼叫 println() 的下方,新增 for 迴圈。在括號中輸入變數 planet 的名稱,透過 solarSystem 清單執行迴圈。
for (planet in solarSystem) {
}
  1. 在大括弧中,使用 println() 顯示 planet 的值。
for (planet in solarSystem) {
    println(planet)
}
  1. 執行程式碼。系統會針對集合中的每個項目執行迴圈本體中的程式碼。
...
Mercury
Venus
Earth
Mars
Jupiter
Saturn
Uranus
Neptune

在清單中新增元素

只有實作 MutableList 介面的類別,才能新增、移除和更新集合中的元素。如果持續追蹤新發現的行星,您可能會希望能經常在清單中加入元素。建立要新增或移除元素的清單時,您需要明確呼叫 mutableListOf() 函式,而不是 listOf()

add() 函式有兩個版本:

  • 第一個 add() 函式有清單中元素類型的單一參數,並且會將其新增至清單結尾。
  • 另一版本的 add() 有兩個參數。第一個參數對應至要插入新元素的索引。第二個參數則是要新增至清單的元素。

現在來看看這兩個版本的實際操作方式。

  1. solarSystem 的初始化作業變更為呼叫 mutableListOf(),而非 listOf()。您現在可以呼叫在 MutableList 中定義的方法。
val solarSystem = mutableListOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
  1. 同樣地,我們可能會想將冥王星分類為行星。在 solarSystem 上呼叫 add() 方法,傳入 "Pluto" 做為單一引數。
solarSystem.add("Pluto")
  1. 有些科學家認為,過去曾有一顆名叫特亞的行星,後來與地球相撞並形成月球。在索引 3 插入 "Theia",介於 "Earth""Mars" 之間。
solarSystem.add(3, "Theia")

更新特定索引的元素

您可以使用下標語法更新現有元素:

  1. 將索引 3 的值更新為 "Future Moon"
solarSystem[3] = "Future Moon"
  1. 使用下標語法顯示索引 39 的值。
println(solarSystem[3])
println(solarSystem[9])
  1. 執行程式碼,驗證輸出內容。
Future Moon
Pluto

移除清單中的元素

您可以使用 remove()removeAt() 方法移除元素,例如將元素傳入 remove() 方法,或使用 removeAt() 依索引移除元素。

現在來看看這兩種元素移除方法的實際操作方式。

  1. 呼叫 solarSystem 上的 removeAt(),傳入 9 以取得索引。這樣就能從清單中移除 "Pluto"
solarSystem.removeAt(9)
  1. 呼叫 solarSystem 上的 remove(),傳入 "Future Moon" 做為要移除的元素。系統會搜尋清單,如果找到相符的元素,就會移除該元素。
solarSystem.remove("Future Moon")
  1. List 提供 contains() 方法,如果清單中有元素,就會傳回 Boolean。顯示針對 "Pluto" 呼叫 contains() 的結果。
println(solarSystem.contains("Pluto"))
  1. 更精簡的語法是使用 in 運算子。您可以使用元素、in 運算子和集合,檢查元素是否在清單中。請使用 in 運算子檢查 solarSystem 是否包含 "Future Moon"
println("Future Moon" in solarSystem)
  1. 執行程式碼。這兩個陳述式都應顯示 false
...
false
false

4. 組合

組合是沒有特定順序,且不允許重複值的集合。

9de9d777e6b1d265.png

為什麼可以有這種集合?答案是「雜湊碼」。雜湊碼是由任何 Kotlin 類別的 hashCode() 方法產生的 Int。這可視為 Kotlin 物件的半唯一 ID。物件的任何微幅變更 (例如在 String 中增加一個字元),都會產生大不相同的雜湊值。雖然兩個物件可能會有相同的雜湊碼 (稱為雜湊衝突),但 hashCode() 函式可確保一定程度的唯一性,而在大多數情況下,兩個不同的值各有不重複的雜湊碼。

84842b78e78f2f58.png

組合有兩個重要特性:

  1. 與清單相比,在集合中搜尋特定元素的速度很快,對大型集合而言更是如此。雖然 ListindexOf() 需從頭檢查元素,直到找到相符項目為止,但平均來說,不論是第一個或是第幾十萬個元素,檢查元素是否在組合中所需的時間都相同。
  2. 組合的記憶體用量通常會高於資料數量相同的清單,因為組合通常需要更多陣列索引,而不是資料。

組合的好處是能確保獨特性。如果您正在編寫用來追蹤新行星的程式,可以利用組合這種簡易做法,檢查行星是否為已知行星。由於資料量龐大,通常建議檢查清單中是否存在某個元素,而這需要疊代所有元素。

就像 ListMutableList,系統同時有 SetMutableSetMutableSet 會實作 Set,因此任何實作 MutableSet 的類別都需要實作這兩者。

691f995fde47f1ff.png

在 Kotlin 中使用 MutableSet

本範例將使用 MutableSet 示範如何新增及移除元素。

  1. main() 中移除現有程式碼。
  2. 使用 mutableSetOf() 建立名為 solarSystemSet 行星。這會傳回 MutableSet,其預設實作方法為 LinkedHashSet()
val solarSystem = mutableSetOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
  1. 使用 size 屬性顯示組合大小。
println(solarSystem.size)
  1. MutableList 一樣,MutableSet 採用 add() 方法。使用 add() 方法在 solarSystem 組合中加入 "Pluto"。只需一個參數即可新增元素。組合中的元素不一定有順序,因此沒有索引!
solarSystem.add("Pluto")
  1. 在新增元素後,顯示組合的 size
println(solarSystem.size)
  1. contains() 函式會使用單一參數,並檢查組合中是否含有指定的元素。如果有,會傳回 true。如果沒有,則會傳回 false。呼叫 contains(),檢查 "Pluto" 是否在 solarSystem 之中。
println(solarSystem.contains("Pluto"))
  1. 執行程式碼。大小增加後,現在 contains() 會傳回 true
8
9
true
  1. 如前所述,組合不能包含重複項目。請再次嘗試新增 "Pluto"
solarSystem.add("Pluto")
  1. 再次顯示組合大小。
println(solarSystem.size)
  1. 再次執行程式碼。"Pluto" 已位於組合中,因此無法新增。這次大小就不會增加。
...
9

remove() 函式會使用單一參數,並從集合中移除指定的元素。

  1. 使用 remove() 函式移除 "Pluto"
solarSystem.remove("Pluto")
  1. 請顯示集合大小,然後再次呼叫 contains(),確認 "Pluto" 是否仍在組合中。
println(solarSystem.size)
println(solarSystem.contains("Pluto"))
  1. 執行程式碼。"Pluto" 已不在組合中,且現在的大小是 8。
...
8
false

5. 對應集合

Map 是由索引鍵和值組成的集合。這就是所謂的「對應」,因為不重複的索引鍵會「對應」到其他值。索引鍵和其相關值通常稱為 key-value pair

8571494fb4a106b6.png

對應中的索引鍵都是不重複的,但對應中的值則不一定。兩個不同的索引鍵可以對應到相同的值。舉例來說,"Mercury"0 個衛星,"Venus" 也有 0 個衛星。

依索引鍵從對應中存取值,通常會比在大型清單中搜尋 (例如使用 indexOf()) 更快。

您可以使用 mapOf()mutableMapOf() 函式宣告對應。對應需要兩個以逗號分隔的泛型類型,一個代表索引鍵,另一個代表值。

affc23a0e1f2b223.png

如果對應具有初始值,也可以使用類型推論。如要在對應中填入初始值,則每個鍵/值組合都包含索引鍵,且後方依序接上 to 運算子和值。請以半形逗號分隔每個鍵/值組合。

2ed99c3391c74ec4.png

以下進一步說明如何使用對應,並介紹一些實用的屬性和方法。

  1. main() 中移除現有程式碼。
  2. 使用 mutableMapOf() 與初始值建立名為 solarSystem 的對應,如下所示。
val solarSystem = mutableMapOf(
    "Mercury" to 0,
    "Venus" to 0,
    "Earth" to 1,
    "Mars" to 2,
    "Jupiter" to 79,
    "Saturn" to 82,
    "Uranus" to 27,
    "Neptune" to 14
)
  1. 就像清單和組合,Map 提供 size 屬性,內含鍵/值組合的數量。顯示 solarSystem 對應的大小。
println(solarSystem.size)
  1. 您可以使用下標語法設定其他鍵/值組合。請將索引鍵 "Pluto" 設為 5 的值。
solarSystem["Pluto"] = 5
  1. 插入元素後,再次顯示大小。
println(solarSystem.size)
  1. 您可以使用下標語法取得值。顯示索引鍵 "Pluto" 的衛星數量。
println(solarSystem["Pluto"])
  1. 您也可以使用 get() 方法存取值。無論您使用下標語法還是呼叫 get(),所傳遞的索引鍵都可能不位於對應中。如果沒有鍵/值組合,系統就會傳回空值。請顯示 "Theia" 的衛星數量。
println(solarSystem.get("Theia"))
  1. 執行程式碼。系統應會顯示冥王星的衛星數量。但由於特亞不在對應中,因此呼叫 get() 會傳回空值。
8
9
5
null

remove() 方法會移除含有指定索引鍵的鍵/值組合。如果指定的索引鍵不在對應中,也會傳回已移除的值或 null

  1. 顯示呼叫 remove() 並傳入 "Pluto" 的結果。
solarSystem.remove("Pluto")
  1. 如要確認項目是否已移除,請再次顯示大小。
println(solarSystem.size)
  1. 執行程式碼。移除項目後的對應大小是 8。
...
8
  1. 下標語法或 put() 方法也可以修改現有索引鍵的值。請使用下標語法將木星的衛星數量更新為 78,然後顯示新的值。
solarSystem["Jupiter"] = 78
println(solarSystem["Jupiter"])
  1. 執行程式碼。現有索引鍵 "Jupiter" 的值已更新。
...
78

6. 結論

恭喜!您已瞭解程式設計中的「陣列」這種基本資料類型,也認識多種從陣列擴展而來的便利集合類型,包括 ListSetMap。這些集合類型可用來在程式碼中分類及整理值。在陣列和清單中,您可以依索引快速存取元素;在集合和對應中,則可使用雜湊碼更輕鬆地在集合中尋找元素。在日後的應用程式中,您會發現這些集合類型的使用頻率很高,您瞭解如何加以利用後,也會對您往後的程式設計職涯有幫助。

摘要

  • 陣列會儲存類型相同的已排序資料,且具有固定大小。
  • 陣列可用於實作許多其他集合類型。
  • 清單為可調整大小的排序集合。
  • 組合是沒有順序的集合,而且不包含重複項目。
  • 對應的運作方式與組合類似,會儲存指定類型的鍵/值組合。

7. 瞭解詳情