1. 簡介
在許多應用程式中,可能已有資料以清單方式顯示,例如聯絡人、設定、搜尋結果等。
然而,在您目前編寫過的程式碼中,您使用的大多數是以單一值組成的資料,就像畫面中顯示的數字或文字。如要建構有不同數量資料的應用程式,您必須瞭解如何使用集合。
集合類型 (有時稱為資料結構) 可讓您以井然有序的方式 (通常是以相同的資料類型分組整理) 儲存多個值。集合可能是已排序的清單、一組不重複的值,或是某種資料類型的值與對應的另一種資料類型的值。能夠有效運用集合的功能,您就可以實作 Android 應用程式的常見功能 (例如:捲動清單),並且可以解決與不同資料數量有關的各種實際程式設計問題。
本程式碼研究室討論如何在程式碼中使用多個值,並介紹多種不同的資料結構,包括陣列、清單、組合以及地圖。
必要條件
- 熟悉 Kotlin 中的物件導向程式設計,包含類別、介面和一般項目。
課程內容
- 如何建立及修改陣列。
- 如何使用
List
和MutableList
。 - 如何使用
Set
和MutableSet
。 - 如何使用
Map
和MutableMap
。
軟硬體需求
- 可存取 Kotlin Playground 的網路瀏覽器。
2. Kotlin 中的陣列
什麼是陣列?
陣列是將程式中不同數量的值分組的最簡單方式。
比如說,太陽能板分組稱為太陽能陣列,或是學習 Kotlin 會開啟程式設計工作機會的陣列,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
的新變數,如圖所示。此陣列可以儲存 9 個元素,而非 8 個。
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
宣告,而是假設為僅會取得。命名方式不限。如果清單的名稱為複數 (例如 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
。列印呼叫contains()
的結果以取得"Pluto"
。
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)
Set
和List
一樣採用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
) 等數個便利的集合類型建構。這些集合類型可讓您分類及整理程式碼中的值。陣列和清單可讓您透過索引快速存取元素,而集合和地圖則使用雜湊碼,讓您可以更輕鬆地在集合中尋找元素。在日後的應用程式中,您會發現這些集合類型都會經常使用,而瞭解如何加以利用更是對您日後在程式設計方面的工作大有益處。
摘要
- 陣列會儲存相同類型的已排序資料,而且大小固定。
- 陣列可用於實作許多其他集合類型。
- 清單為可調整大小的排序集合。
- 組合是沒有排序的集合,而且不包含重複項目。
- 地圖的運作方式與組合類似,而且會儲存指定類型的鍵/值組合。