1. 簡介
在許多應用程式中,可能都有以清單顯示的資料,例如聯絡人、設定、搜尋結果等。
但在您目前編寫的程式碼中,大多是使用以單一值組成的資料,例如在畫面上顯示的數字或文字。如要建構資料量不固定的應用程式,您需要瞭解如何使用集合。
利用集合類型 (有時稱為資料結構),您就能井然有序地儲存多個值 (通常是資料類型相同的值)。集合可能是已排序的清單、一組不重複的值,或是兩種資料類型的值之間的對應關係。如果能有效運用集合,就可以實作 Android 應用程式的常見功能 (例如捲動清單),並解決與資料量不固定有關的各種實際程式設計問題。
本程式碼研究室討論如何在程式碼中使用多個值,並介紹多種不同的資料結構,包括陣列、清單、組合和對應。
必要條件
- 熟悉 Kotlin 中的物件導向程式設計,包含類別、介面和泛型。
課程內容
- 如何建立及修改陣列。
- 如何使用
List
和MutableList
。 - 如何使用
Set
和MutableSet
。 - 如何使用
Map
和MutableMap
。
軟硬體需求
- 可存取 Kotlin Playground 的網路瀏覽器。
2. Kotlin 中的陣列
什麼是陣列?
如要將程式中任意數量的值分組,使用陣列是最簡單的做法。
就像一組太陽能板稱為太陽能陣列 (solar array)、學習 Kotlin 可獲得一系列 (an array of) 程式設計工作機會,Array
代表的就是多個值。具體而言,陣列是指一系列具有相同資料類型的值。
- 陣列包含多個稱為「元素」的值,有時也稱為「項目」。
- 陣列中的元素都會排序,可透過索引存取。
什麼是索引?索引是與陣列元素相對應的整數。索引會指出項目與陣列中起始元素之間的距離,這稱為以零為開頭的索引。陣列中第一個元素的索引為 0,則第二個元素的索引為 1,因為它與第一個元素距離一個位置,以此類推。
在裝置記憶體中,陣列中的元素會依序排列儲存。本程式碼研究室不會說明基礎細節,但上述資訊有以下兩項重要涵義:
- 有了索引,就能快速存取陣列元素。您可以依索引隨機存取陣列的任一元素,所需時間也會與隨機存取其他任一元素相同,因此會說陣列具有「隨機存取」機制。
- 陣列有固定大小。也就是說,加入陣列的元素數量無法超過此大小。若在大小為 100 個元素的陣列中嘗試存取索引 100 的元素,系統會擲回例外狀況,因為最高索引為 99 (提醒您,第一個索引是 0,不是 1)。不過,您可以修改陣列中位於索引的值。
如要在程式碼中宣告陣列,請使用 arrayOf()
函式。
arrayOf()
函式會將陣列元素視為參數,然後傳回與所傳入參數相符的陣列類型。由於 arrayOf()
的參數數量不盡相同,這或許會與您看到的其他函式略有不同。如果將兩個引數傳入 arrayOf()
,則產生的陣列會包含兩個元素,索引分別是 0 和 1。如果傳入三個引數,則產生的陣列將有 3 個元素,索引是 0 到 2。
現在我們來瞭解陣列的實際運作情形,順便探索一下太陽系吧!
- 前往 Kotlin Playground。
- 在
main()
中建立rockPlanets
變數。呼叫arrayOf()
,然後傳入類型String
與四個字串,分別代表太陽系中的岩石行星。
val rockPlanets = arrayOf<String>("Mercury", "Venus", "Earth", "Mars")
- 由於 Kotlin 會使用類型推論,因此您可以在呼叫
arrayOf()
時省略類型名稱。在rockPlanets
變數下方新增另一個變數gasPlanets
,而不將類型傳入角括號。
val gasPlanets = arrayOf("Jupiter", "Saturn", "Uranus", "Neptune")
- 您可以利用陣列執行一些有趣的操作。舉例來說,就像數字類型
Int
或Double
一樣,您可以同時加入兩個陣列。請建立名為solarSystem
的新變數,然後使用加號 (+
) 運算子,將新變數設為等於rockPlanets
和gasPlanets
的結果。結果會是新的陣列包含rockPlanets
陣列的所有元素,以及和gasPlanets
陣列的元素。
val solarSystem = rockPlanets + gasPlanets
- 執行程式,確認能否正常運作。目前還不應顯示任何輸出內容。
存取陣列中的元素
您可以依索引存取陣列的元素。
這就是下標語法。其中包含三個部分:
- 陣列名稱。
- 左方括號 (
[
) 和右方括號 (]
)。 - 位於方括號中的陣列元素索引。
接下來要利用索引存取 solarSystem
陣列的元素。
- 在
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])
- 執行程式。元素的順序與您呼叫
arrayOf()
時的順序相同。
Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune
您也可以透過索引設定陣列元素的值。
存取索引的方法與先前相同:先是陣列名稱,然後是包含索引的左方括號和右方括號。然後是指派運算子 (=
) 和一個新的值。
現在來練習修改 solarSystem
陣列的值。
- 我們可以為火星重新命名,供未來的人類移民使用。請存取索引
3
的元素,然後設為"Little Earth"
。
solarSystem[3] = "Little Earth"
- 顯示索引
3
的元素。
println(solarSystem[3])
- 執行程式。陣列的第四個元素 (位於索引
3
) 已更新。
... Little Earth
- 現在,假設科學家發現海王星之後的第九顆行星,稱為「冥王星」。我們先前提過,陣列的大小無法調整。如果嘗試調整會怎麼樣?我們來嘗試將冥王星新增至
solarSystem
陣列。新增冥王星至索引8
,因為這是陣列中的第 9 個元素。
solarSystem[8] = "Pluto"
- 執行程式碼。系統會擲回
ArrayIndexOutOfBounds
例外狀況。由於陣列已有 8 個元素,因此如同預期,您無法直接加入第 9 個元素。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
- 移除在陣列中加入的冥王星。
要移除的程式碼
solarSystem[8] = "Pluto"
- 如要讓陣列大小大於現有陣列,您需要建立新陣列。定義名為
newSolarSystem
的新變數,如下所示。此陣列可儲存九個元素,而非八個。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
- 現在嘗試顯示索引
8
的元素。
println(newSolarSystem[8])
- 執行程式碼,然後就會發現程式碼執行時沒有任何例外狀況。
... Pluto
做得好!現在您對陣列已有所瞭解,可以利用集合執行幾乎所有操作。
等等,不是馬上開始!雖然陣列是程式設計的基本面向之一,但如果工作需要新增和移除元素、確保集合內容不重複或將物件對應至其他物件,使用陣列並不是簡單直接的做法,而且應用程式程式碼會很快變成一團亂。
因此,大部分的程式設計語言 (包括 Kotlin) 都採用特殊的集合類型,處理實際應用程式中常見的情況。以下各節說明三種常用的集合:List
、Set
和 Map
。此外,您也會瞭解常見的屬性和方法,以及這些集合類型的使用情境。
3. 清單
清單是經過排序且可調整大小的集合,通常是實作為可調整大小的陣列。陣列填滿時,如果嘗試插入新元素,系統會將陣列複製到較大的新陣列。
有了清單,您也可以在其他元素之間,於特定索引插入新元素。
這就是能在清單中新增與移除元素的方式。大多數情況下,無論清單元素的數量為何,在清單中加入任何元素所需的時間都一樣。每隔一段時間,如果加入新元素會導致陣列超過定義的大小,就可能必須移動陣列元素,才能為新元素騰出空間。清單會自動完成上述所有操作,但實際上只是系統在需要時將陣列換新而已。
List
和 MutableList
您將在 Kotlin 中看到的集合類型會實作一或多個介面。如本單元之前的「泛型、物件和擴充功能」程式碼研究室所述,介面提供類別可實作的標準屬性和方法組合。實作 List
介面的類別可提供實作內容給 List
介面的所有屬性和方法。MutableList
也是如此。
那麼 List
和 MutableList
有哪些功能?
List
這種介面可定義與項目的唯讀排序集合相關的屬性和方法。MutableList
會定義修改清單的方法 (例如新增及移除元素),即可擴充List
介面。
這些介面僅指定 List
和/或 MutableList
的屬性與方法。每個屬性和方法的實作方法,是由擴充介面的類別決定。如果不是每次都使用,上述以陣列為基礎的實作方法也仍會是您最常用的實作方式,但 Kotlin 也允許其他類別擴充 List
和 MutableList
。
listOf()
函式
和 arrayOf()
一樣,listOf()
函式會以項目做為參數,但會傳回 List
,而非陣列。
- 從
main()
中移除現有程式碼。 - 在
main()
中呼叫listOf()
,建立名為solarSystem
的行星List
。
fun main() {
val solarSystem = listOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
}
List
具有size
屬性,可取得清單中的元素數量。顯示solarSystem
清單的size
。
println(solarSystem.size)
- 執行程式碼。清單大小應為 8。
8
從清單中存取元素
就像陣列一樣,您可以使用下標語法,從 List
存取特定索引的元素。您也可以使用 get()
方法執行相同操作。下標語法和 get()
方法都會使用 Int
做為參數,並傳回該索引的元素。和 Array
一樣,ArrayList
是以零為開頭的索引,因此第四個元素會位於索引 3
。
- 使用下標語法顯示位於索引
2
的行星。
println(solarSystem[2])
- 呼叫
solarSystem
清單中的get()
,顯示位於索引3
的元素。
println(solarSystem.get(3))
- 執行程式碼。位於索引
2
的元素為"Earth"
,位於索引3
的元素則為"Mars"
。
... Earth Mars
除了依索引取得元素以外,您也可以使用 indexOf()
方法搜尋特定元素的索引。indexOf()
方法會搜尋清單的特定元素 (以引數形式傳入),然後傳回該元素第一次出現的索引。如果清單中沒有出現該元素,則會傳回 -1
。
- 顯示
solarSystem
中呼叫indexOf()
傳入"Earth"
的結果。
println(solarSystem.indexOf("Earth"))
- 呼叫傳入
"Pluto"
的indexOf()
,然後顯示結果。
println(solarSystem.indexOf("Pluto"))
- 執行程式碼。一個元素與
"Earth"
相符,因此顯示索引2
。沒有與"Pluto"
相符的元素,因此顯示-1
。
... 2 -1
使用 for
迴圈疊代清單元素
瞭解函式類型和 lambda 運算式後,就能利用 repeat()
函式多次執行程式碼。
程式設計的常見工作,是對清單中的每個元素執行一次工作。Kotlin 包含一項稱為 for
迴圈的功能,可透過簡潔清楚的語法完成這項作業。這通常稱為透過清單執行「迴圈」,或是「疊代」清單。
如要透過清單執行迴圈,請使用 for
關鍵字,並在後面加上一對左右括號。在括號中加入變數名稱,後方依序加上 in
關鍵字和集合名稱。右括號之後是一對大括弧,您可以在大括弧中加入要針對集合中每個元素執行的程式碼。這就是迴圈「本體」。每次執行這個程式碼,就稱為「疊代」。
in
關鍵字之前的變數不會以 val
或 var
宣告,而是假設為 get-only。命名方式不限。如果清單的名稱為複數 (例如 planets
),則變數通常會以單數格式命名 (例如 planet
)。將變數命名為 item
或 element
也很常見。
這會用做與集合中目前元素相對應的暫時變數,也就是索引 0
的元素適用於第一次疊代、索引 1
的元素適用於第二次疊代,以此類推,並且可在大括弧中存取。
如要查看此操作,請使用 for
迴圈顯示出各個行星名稱,且每行單獨顯示一個名稱。
- 在
main()
中最近呼叫println()
的下方,新增for
迴圈。在括號中輸入變數planet
的名稱,透過solarSystem
清單執行迴圈。
for (planet in solarSystem) {
}
- 在大括弧中,使用
println()
顯示planet
的值。
for (planet in solarSystem) {
println(planet)
}
- 執行程式碼。系統會針對集合中的每個項目執行迴圈本體中的程式碼。
... Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune
在清單中新增元素
只有實作 MutableList
介面的類別,才能新增、移除和更新集合中的元素。如果持續追蹤新發現的行星,您可能會希望能經常在清單中加入元素。建立要新增或移除元素的清單時,您需要明確呼叫 mutableListOf()
函式,而不是 listOf()
。
add()
函式有兩個版本:
- 第一個
add()
函式有清單中元素類型的單一參數,並且會將其新增至清單結尾。 - 另一版本的
add()
有兩個參數。第一個參數對應至要插入新元素的索引。第二個參數則是要新增至清單的元素。
現在來看看這兩個版本的實際操作方式。
- 將
solarSystem
的初始化作業變更為呼叫mutableListOf()
,而非listOf()
。您現在可以呼叫在MutableList
中定義的方法。
val solarSystem = mutableListOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
- 同樣地,我們可能會想將冥王星分類為行星。在
solarSystem
上呼叫add()
方法,傳入"Pluto"
做為單一引數。
solarSystem.add("Pluto")
- 有些科學家認為,過去曾有一顆名叫特亞的行星,後來與地球相撞並形成月球。在索引
3
插入"Theia"
,介於"Earth"
和"Mars"
之間。
solarSystem.add(3, "Theia")
更新特定索引的元素
您可以使用下標語法更新現有元素:
- 將索引
3
的值更新為"Future Moon"
。
solarSystem[3] = "Future Moon"
- 使用下標語法顯示索引
3
和9
的值。
println(solarSystem[3])
println(solarSystem[9])
- 執行程式碼,驗證輸出內容。
Future Moon Pluto
移除清單中的元素
您可以使用 remove()
或 removeAt()
方法移除元素,例如將元素傳入 remove()
方法,或使用 removeAt()
依索引移除元素。
現在來看看這兩種元素移除方法的實際操作方式。
- 呼叫
solarSystem
上的removeAt()
,傳入9
以取得索引。這樣就能從清單中移除"Pluto"
。
solarSystem.removeAt(9)
- 呼叫
solarSystem
上的remove()
,傳入"Future Moon"
做為要移除的元素。系統會搜尋清單,如果找到相符的元素,就會移除該元素。
solarSystem.remove("Future Moon")
List
提供contains()
方法,如果清單中有元素,就會傳回Boolean
。顯示針對"Pluto"
呼叫contains()
的結果。
println(solarSystem.contains("Pluto"))
- 更精簡的語法是使用
in
運算子。您可以使用元素、in
運算子和集合,檢查元素是否在清單中。請使用in
運算子檢查solarSystem
是否包含"Future Moon"
。
println("Future Moon" in solarSystem)
- 執行程式碼。這兩個陳述式都應顯示
false
。
... false false
4. 組合
組合這種集合沒有特定順序,且不允許有重複的值。
為什麼可以有這種集合?答案是「雜湊碼」。雜湊碼是由任何 Kotlin 類別的 hashCode()
方法產生的 Int
。這可視為 Kotlin 物件的半唯一 ID。物件的任何微幅變更 (例如在 String
中增加一個字元),都會產生大不相同的雜湊值。雖然兩個物件可能會有相同的雜湊碼 (稱為雜湊衝突),但 hashCode()
函式可確保一定程度的唯一性,而在大多數情況下,兩個不同的值各有不重複的雜湊碼。
組合有兩個重要特性:
- 與清單相比,在集合中搜尋特定元素的速度很快,對大型集合而言更是如此。雖然
List
的indexOf()
需從頭檢查元素,直到找到相符項目為止,但平均來說,不論是第一個或是第幾十萬個元素,檢查元素是否在組合中所需的時間都相同。 - 組合的記憶體用量通常會高於資料數量相同的清單,因為組合通常需要更多陣列索引,而不是資料。
組合的好處是能確保獨特性。如果您正在編寫用來追蹤新行星的程式,可以利用組合這種簡易做法,檢查行星是否為已知行星。由於資料量龐大,通常建議檢查清單中是否存在某個元素,而這需要疊代所有元素。
就像 List
和 MutableList
,系統同時有 Set
和 MutableSet
。MutableSet
會實作 Set
,因此任何實作 MutableSet
的類別都需要實作這兩者。
在 Kotlin 中使用 MutableSet
本範例將使用 MutableSet
示範如何新增及移除元素。
- 從
main()
中移除現有程式碼。 - 使用
mutableSetOf()
建立名為solarSystem
的Set
行星。這會傳回MutableSet
,其預設實作方法為LinkedHashSet()
。
val solarSystem = mutableSetOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
- 使用
size
屬性顯示組合大小。
println(solarSystem.size)
- 和
List
一樣,Set
採用add()
方法。使用add()
方法在solarSystem
組合中加入"Pluto"
。只需一個參數即可新增元素。組合中的元素不一定有順序,因此沒有索引!
solarSystem.add("Pluto")
- 在新增元素後,顯示組合的
size
。
println(solarSystem.size)
contains()
函式會使用單一參數,並檢查組合中是否含有指定的元素。如果有,會傳回 true。如果沒有,則會傳回 false。呼叫contains()
,檢查"Pluto"
是否在solarSystem
之中。
println(solarSystem.contains("Pluto"))
- 執行程式碼。大小增加後,現在
contains()
會傳回true
。
8 9 true
- 如前所述,組合不能包含重複項目。請再次嘗試新增
"Pluto"
。
solarSystem.add("Pluto")
- 再次顯示組合大小。
println(solarSystem.size)
- 再次執行程式碼。
"Pluto"
已位於組合中,因此無法新增。這次大小就不會增加。
... 9
remove()
函式會使用單一參數,並從集合中移除指定的元素。
- 使用
remove()
函式移除"Pluto"
。
solarSystem.remove("Pluto")
- 請顯示集合大小,然後再次呼叫
contains()
,確認"Pluto"
是否仍在組合中。
println(solarSystem.size)
println(solarSystem.contains("Pluto"))
- 執行程式碼。
"Pluto"
已不在組合中,且現在的大小是 8。
... 8 false
5. 對應集合
Map
是由索引鍵和值組成的集合。這就是所謂的「對應」,因為不重複的索引鍵會「對應」到其他值。索引鍵和其相關值通常稱為 key-value pair
。
對應中的索引鍵都是不重複的,但對應中的值則不一定。兩個不同的索引鍵可以對應到相同的值。舉例來說,"Mercury"
有 0
個衛星,"Venus"
也有 0
個衛星。
依索引鍵從對應中存取值,通常會比在大型清單中搜尋 (例如使用 indexOf()
) 更快。
您可以使用 mapOf()
或 mutableMapOf()
函式宣告對應。對應需要兩個以逗號分隔的泛型類型,一個代表索引鍵,另一個代表值。
如果對應具有初始值,也可以使用類型推論。如要在對應中填入初始值,則每個鍵/值組合都包含索引鍵,且後方依序接上 to
運算子和值。請以半形逗號分隔每個鍵/值組合。
以下進一步說明如何使用對應,並介紹一些實用的屬性和方法。
- 從
main()
中移除現有程式碼。 - 使用
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
)
- 就像清單和組合,
Map
提供size
屬性,內含鍵/值組合的數量。顯示solarSystem
對應的大小。
println(solarSystem.size)
- 您可以使用下標語法設定其他鍵/值組合。請將索引鍵
"Pluto"
設為5
的值。
solarSystem["Pluto"] = 5
- 插入元素後,再次顯示大小。
println(solarSystem.size)
- 您可以使用下標語法取得值。顯示索引鍵
"Pluto"
的衛星數量。
println(solarSystem["Pluto"])
- 您也可以使用
get()
方法存取值。無論您使用下標語法還是呼叫get()
,所傳遞的索引鍵都可能不位於對應中。如果沒有鍵/值組合,系統就會傳回空值。請顯示"Theia"
的衛星數量。
println(solarSystem["Theia"])
- 執行程式碼。系統應會顯示冥王星的衛星數量。但由於特亞不在對應中,因此呼叫
get()
會傳回空值。
8 9 5 null
remove()
方法會移除含有指定索引鍵的鍵/值組合。如果指定的索引鍵不在對應中,也會傳回已移除的值或 null
。
- 顯示呼叫
remove()
並傳入"Pluto"
的結果。
solarSystem.remove("Pluto")
- 如要確認項目是否已移除,請再次顯示大小。
println(solarSystem.size)
- 執行程式碼。移除項目後的對應大小是 8。
... 8
- 下標語法或
put()
方法也可以修改現有索引鍵的值。請使用下標語法將木星的衛星數量更新為 78,然後顯示新的值。
solarSystem["Jupiter"] = 78
println(solarSystem["Jupiter"])
- 執行程式碼。現有索引鍵
"Jupiter"
的值已更新。
... 78
6. 結論
恭喜!您已瞭解程式設計中的「陣列」這種基本資料類型,也認識多種從陣列擴展而來的便利集合類型,包括 List
、Set
和 Map
。這些集合類型可用來在程式碼中分類及整理值。在陣列和清單中,您可以依索引快速存取元素;在集合和對應中,則可使用雜湊碼更輕鬆地在集合中尋找元素。在日後的應用程式中,您會發現這些集合類型的使用頻率很高,您瞭解如何加以利用後,也會對您往後的程式設計職涯有幫助。
摘要
- 陣列會儲存類型相同的已排序資料,且具有固定大小。
- 陣列可用於實作許多其他集合類型。
- 清單為可調整大小的排序集合。
- 組合是沒有順序的集合,而且不包含重複項目。
- 對應的運作方式與組合類似,會儲存指定類型的鍵/值組合。