在 Kotlin 中使用集合

1. 簡介

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

9fbd3bf7cb6adc72.png

然而,在您目前編寫過的程式碼中,您使用的大多數是以單一值組成的資料,就像畫面中顯示的數字或文字。如要建構有不同數量資料的應用程式,您必須瞭解如何使用集合。

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

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

必要條件

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

課程內容

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

軟硬體需求

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

2. Kotlin 中的陣列

什麼是陣列?

陣列是將程式中不同數量的值分組的最簡單方式。

比如說,太陽能板分組稱為太陽能陣列,或是學習 Kotlin 會開啟程式設計工作機會的陣列,Array 代表多個值。具體而言,陣列是指一系列的資料,且其值都是相同的類型。

960e34f4c96e2fd9.png

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

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

5baf880a3670720d.png

在裝置的記憶體中,陣列中的元素會排列儲存。雖然基礎詳細資料不在此程式碼研究室的範圍之內,但這以下有兩個重要的影響:

  • 透過索引可以快速存取陣列元素。您可以依據索引存取陣列的任何隨機元素,而且存取任何其他隨機元素所需的時間大約都會相同。因此才會說陣列有「隨機存取」
  • 陣列有固定的尺寸。也就是說,陣列無法加入超過此尺寸的元素。嘗試在 100 元素陣列的索引 100 存取元素將擲回例外狀況,而這是因為最高索引為 99 (提醒您,第一個索引是 0,不是 1)。但是,您可以修陣列中索引的值。

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

9d5c8c00b30850cb.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 的新變數,如圖所示。此陣列可以儲存 9 個元素,而非 8 個。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
  1. 現在嘗試列印索引 8 的元素。
println(newSolarSystem[8])
  1. 執行程式碼,然後就會發現程式碼執行時沒有任何例外狀況。
...
Pluto

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

等等,還沒有說完!雖然陣列是程式設計的一個基本部分,使用陣列執行需要新增和移除元素、集合中的唯一性或對應物件與其他物件的工作,卻不是那麼簡單或直接,而且應用程式的程式碼很快也會變成一團亂。

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

3. 清單

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

a4970d42cd1d2b66.png

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

27afd8dd880e1ae5.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 迴圈的功能,可透過簡單清楚的語法完成這項操作。這通常會透過清單或清單的「疊代」稱為「迴圈」

1245a226a9ceeba1.png

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

in 關鍵字之前的變數不會以 valvar 宣告,而是假設為僅會取得。命名方式不限。如果清單的名稱為複數 (例如 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。列印呼叫 contains() 的結果以取得 "Pluto"
println(solarSystem.contains("Pluto"))
  1. 更精簡的語法是使用 in 運算子。您可以使用元素、in 運算子和集合檢查元素是否在清單中。使用 in 運算子檢查 solarSystem 是否包含 "Future Moon"
println("Future Moon" in solarSystem)
  1. 執行程式碼。這兩個陳述式都應列印 false
...
false
false

4. 組合

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

ce127adf37662aa4.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. SetList 一樣採用 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 運算子和值。請以半形逗號分隔每個組合。

8719ffc353f652f.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["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. 瞭解詳情