Kotlin 集合滿豐富的
今日重點:認識 Kotlin 的
Array、List、Set、Map,並深入學習Set的去除重複、查找、篩選、轉換及集合運算。
在 App 開發中,我們很少只處理一筆資料。例如會員清單、商品資料、購物車、感測器數值及藍牙裝置列表,都必須使用陣列或集合統一管理。Kotlin 提供 List、Set、Map 三種主要集合,而 Array 則是固定長度的陣列資料結構。參考資料:Kotlin Collections Overview

雖然它們都能保存多筆資料,但資料結構、是否允許重複及存取方式並不相同。參考資料:Kotlin Collections Overview
| 資料結構 | 是否允許重複 | 是否具有索引 | 長度是否固定 | 主要用途 |
|---|---|---|---|---|
Array |
是 | 是 | 是 | 固定數量資料 |
List |
是 | 是 | 否 | 有順序的資料清單 |
Set |
否 | 不應依賴索引 | 否 | 不可重複的唯一資料 |
Map |
Key 不可重複 | 使用 Key | 否 | Key 與 Value 對應資料 |
選擇資料結構時,可以先問自己三個問題:資料能不能重複、需不需要依照位置存取,以及是否要透過某個名稱或編號尋找資料。參考資料:Kotlin Collection Types
Array 是固定長度且具有順序的資料結構,每個元素都有索引,第一個元素的索引為 0。陣列建立後不能直接增加或刪除元素,但可以修改既有位置的內容。參考資料:Kotlin Arrays
fun main() {
// 建立 IntArray。
val ages: IntArray =
intArrayOf(
22,
23,
25,
26
)
// 使用索引讀取資料。
println("索引 1:${ages[1]}")
println("索引 3:${ages[3]}")
// 使用 indices 取得合法索引範圍。
for (index in ages.indices) {
println(
"ages[$index] = ${ages[index]}"
)
}
println()
// 不需要索引時,可以直接取出元素。
for (age in ages) {
println("年齡:$age")
}
}
執行結果:
索引 1:23
索引 3:26
ages[0] = 22
ages[1] = 23
ages[2] = 25
ages[3] = 26
年齡:22
年齡:23
年齡:25
年齡:26
Kotlin 另外提供 IntArray、FloatArray、DoubleArray、BooleanArray 等基本型別陣列,可以避免在 JVM 上產生不必要的裝箱與拆箱成本。參考資料:Kotlin Arrays-Primitive-type arrays
fun main() {
val scores: FloatArray =
floatArrayOf(
100.5f,
125.3f,
55.7f
)
val passResults: BooleanArray =
booleanArrayOf(
true,
false,
true,
true
)
for (index in scores.indices) {
println(
"scores[$index] = ${scores[index]}"
)
}
println()
for (index in passResults.indices) {
println(
"passResults[$index] = ${passResults[index]}"
)
}
}
建立陣列時,可以提供陣列長度及初始化 Lambda。Lambda 的 it 代表目前元素的索引。參考資料:Kotlin Array Constructor
fun main() {
// it 是索引,因此內容為 0、1、2、3、4。
val numbers1 =
IntArray(5) { index ->
index
}
// 每個位置都填入 10。
val numbers2 =
IntArray(5) {
10
}
// 建立一般 String 陣列。
val messages =
Array(5) { index ->
"$index value"
}
println(numbers1.contentToString())
println(numbers2.contentToString())
println(messages.contentToString())
}
執行結果:
[0, 1, 2, 3, 4]
[10, 10, 10, 10, 10]
[0 value, 1 value, 2 value, 3 value, 4 value]
對陣列使用 == 時,不適合拿來判斷兩個不同陣列的元素內容是否完全相同;比較陣列內容應使用 contentEquals(),顯示內容則可使用 contentToString()。參考資料:Kotlin Arrays-Compare arrays
fun main() {
val array1 =
intArrayOf(
2,
3,
4,
5
)
val array2 =
intArrayOf(
2,
3,
4,
5
)
val array3 = array1
// 不同陣列物件。
println(array1 == array2)
// 指向同一個陣列物件。
println(array1 == array3)
// 比較元素內容。
println(
array1.contentEquals(array2)
)
}
執行結果:
false
true
true
List 是具有順序的集合,每個元素都有索引,而且允許保存重複資料。使用 listOf() 建立唯讀 List,使用 mutableListOf() 則建立可以新增、修改及刪除元素的 MutableList。參考資料:Kotlin List
fun main() {
val emptyList =
listOf<Int>()
val numberList =
listOf(
11,
12,
13,
14,
15
)
val nameList =
listOf(
"Mary",
"John",
"Nancy"
)
val mixedList =
listOf<Any>(
"Mary",
50,
162.3f
)
println(emptyList)
println(numberList)
println(nameList)
println(mixedList)
println("元素數量:${numberList.size}")
println("索引 1:${numberList[1]}")
println("索引 3:${numberList.get(3)}")
}
執行結果:
[]
[11, 12, 13, 14, 15]
[Mary, John, Nancy]
[Mary, 50, 162.3]
元素數量:5
索引 1:12
索引 3:14
MutableList 繼承 List 的讀取能力,另外提供 add()、addAll()、set()、remove()、removeAt() 及 clear() 等修改操作。參考資料:Kotlin Collection Write Operations
fun main() {
val numbers =
mutableListOf(
11,
12,
13,
14,
15
)
// 加到最後面。
numbers.add(20)
println(numbers)
// 在索引 1 插入 30。
numbers.add(
1,
30
)
println(numbers)
// 從索引 3 開始加入多筆資料。
numbers.addAll(
3,
listOf(
1,
2,
3
)
)
println(numbers)
// 將索引 0 的內容改成 100。
numbers[0] = 100
println(numbers)
// 刪除數值 15。
numbers.remove(15)
println(numbers)
// 刪除索引 0 的元素。
numbers.removeAt(0)
println(numbers)
}
執行結果:
[11, 12, 13, 14, 15, 20]
[11, 30, 12, 13, 14, 15, 20]
[11, 30, 12, 1, 2, 3, 13, 14, 15, 20]
[100, 30, 12, 1, 2, 3, 13, 14, 15, 20]
[100, 30, 12, 1, 2, 3, 13, 14, 20]
[30, 12, 1, 2, 3, 13, 14, 20]
Set 最重要的特性是每個元素必須唯一。當建立 Set 時放入重複資料,重複項目只會保留一份。參考資料:Kotlin Set API

fun main() {
val numbers =
setOf(
11,
22,
33,
44,
44,
55
)
val names =
setOf(
"AA",
"AA",
"BB",
"BB"
)
println(numbers)
println(names)
}
執行結果:
[11, 22, 33, 44, 55]
[AA, BB]
雖然常見的 setOf() 在執行結果中會維持插入順序,但 Set 的核心語意是「元素唯一」,並不是使用索引管理資料。因此,若程式邏輯需要長期依賴第幾個元素,通常應該使用 List。參考資料:Kotlin Set API
Kotlin 可以由編譯器推斷 Set 的型別,也可以由開發者明確指定泛型型別。參考資料:Kotlin Constructing Collections
fun main() {
// 明確指定 Int 型別。
val numberSet: Set<Int> =
setOf(
11,
22,
33
)
// 明確指定 String 型別。
val nameSet: Set<String> =
setOf(
"Mary",
"John",
"Nancy"
)
// 建立空的 Double Set。
val emptySet: Set<Double> =
emptySet()
// 使用 Any 允許保存不同型別。
val mixedSet: Set<Any> =
setOf(
"Mary",
20,
52.3
)
println(numberSet)
println(nameSet)
println(emptySet)
println(mixedSet)
}
執行結果:
[11, 22, 33]
[Mary, John, Nancy]
[]
[Mary, 20, 52.3]
雖然 Set<Any> 可以混合保存不同型別,但實務上通常應讓集合元素保持相同型別,才能獲得更好的型別安全及更清楚的程式設計。參考資料:Kotlin Generics
size 屬性及不帶條件的 count() 都能取得集合中的元素數量。參考資料:Kotlin Collection Operations
fun main() {
val names =
setOf(
"AA",
"AA",
"BB",
"BB"
)
val mixedData =
setOf(
"Mary",
20,
52.3
)
println(names.size)
println(mixedData.count())
}
執行結果:
2
3
Kotlin 集合提供 elementAt()、elementAtOrNull() 及 elementAtOrElse() 等單一元素讀取函式,但對 Set 使用這些函式時,是依照迭代順序尋找元素,不代表 Set 本身具有和 List 相同的索引語意。參考資料:Kotlin Retrieve Single Elements
fun main() {
val numbers =
setOf(
11,
22,
33,
44,
55
)
// 取得迭代順序中的第 3 個元素。
println(
numbers.elementAt(2)
)
// 索引不存在時回傳 null。
println(
numbers.elementAtOrNull(10)
)
// 搭配 Elvis 運算子提供預設值。
println(
numbers.elementAtOrNull(10)
?: "Not Found"
)
// 索引不存在時執行 Lambda。
println(
numbers.elementAtOrElse(10) {
-1
}
)
}
執行結果:
33
null
Not Found
-1
如果只是要判斷某個值是否存在,應優先使用 contains() 或 in,而不是先尋找它位於第幾個位置。參考資料:Kotlin Collection Operations
fun main() {
val numbers =
setOf(
11,
22,
33,
44,
55
)
println(numbers.contains(33))
println(55 in numbers)
println(100 !in numbers)
}
執行結果:
true
true
true
所有 Kotlin 集合都能使用 for 或 forEach() 逐一處理元素;需要同時取得迭代編號時,則可使用 forEachIndexed()。參考資料:Kotlin Iterators
fun main() {
val names =
setOf(
"AA",
"BB"
)
// 使用 for。
for (name in names) {
println(name)
}
println()
// 使用 forEach。
names.forEach { name ->
println(name)
}
println()
// 同時取得迭代編號及元素。
names.forEachIndexed { index, name ->
println("$index:$name")
}
}
執行結果:
AA
BB
AA
BB
0:AA
1:BB
find() 會回傳第一個符合條件的元素,findLast() 則回傳最後一個符合條件的元素;找不到時,兩者都會回傳 null。參考資料:Kotlin Retrieve Single Elements
fun main() {
val numbers =
setOf(
11,
22,
33,
44,
55
)
val firstResult =
numbers.find { number ->
number > 30
}
val lastResult =
numbers.findLast { number ->
number > 30
}
println(firstResult)
println(lastResult)
}
執行結果:
33
55
filter() 會逐一將元素交給 Lambda;Lambda 回傳 true 時保留元素,回傳 false 時排除元素。需要注意的是,對 Set 使用 filter() 後,結果型別是 List。參考資料:Kotlin Filtering Collections
fun main() {
val numbers =
setOf(
11,
22,
33,
44,
55
)
val filteredNumbers: List<Int> =
numbers.filter { number ->
number > 30
}
println(filteredNumbers)
}
執行結果:
[33, 44, 55]
若希望篩選結果繼續保持 Set 型別,可以再呼叫 toSet()。參考資料:Kotlin Collection Transformations
fun main() {
val numbers =
setOf(
11,
22,
33,
44,
55
)
val filteredSet: Set<Int> =
numbers
.filter { number ->
number > 30
}
.toSet()
println(filteredSet)
}
執行結果:
[33, 44, 55]
setOf() 建立的是唯讀 Set;如果需要增加或刪除元素,必須使用 mutableSetOf() 建立 MutableSet。參考資料:Kotlin Collection Write Operations
fun main() {
val numbers =
mutableSetOf(
11,
22,
33,
44
)
println(numbers)
// 新增單一元素。
numbers.add(55)
println(numbers)
// 再次加入 55,不會重複保存。
numbers.add(55)
println(numbers)
// 新增多個元素。
numbers.addAll(
setOf(
55,
66
)
)
println(numbers)
// 刪除單一元素。
numbers.remove(11)
println(numbers)
// 刪除多個元素。
numbers.removeAll(
setOf(
33,
55
)
)
println(numbers)
}
執行結果:
[11, 22, 33, 44]
[11, 22, 33, 44, 55]
[11, 22, 33, 44, 55]
[11, 22, 33, 44, 55, 66]
[22, 33, 44, 55, 66]
[22, 44, 66]
plus()、minus()、+ 及 - 會根據原集合產生結果集合,不會直接修改原集合;add()、remove() 則是 MutableSet 的原地修改操作。參考資料:Kotlin Plus and Minus Operators
fun main() {
val originalSet =
mutableSetOf(
1,
2,
3,
4,
5
)
val plusResult =
originalSet.plus(6)
val minusResult =
originalSet.minus(2)
println("原集合:$originalSet")
println("加入 6:$plusResult")
println("移除 2:$minusResult")
}
執行結果:
原集合:[1, 2, 3, 4, 5]
加入 6:[1, 2, 3, 4, 5, 6]
移除 2:[1, 3, 4, 5]
使用 toSet() 可以將 List 轉為 Set 並移除重複元素;使用 toList() 或 toMutableList() 則可將 Set 轉成具有索引概念的清單。參考資料:Kotlin Collection Transformations
fun main() {
val originalList =
mutableListOf(
1,
1,
2,
2,
3,
4,
5,
5
)
println("原始 List:$originalList")
val uniqueSet =
originalList.toSet()
println("轉成 Set:$uniqueSet")
val newList =
uniqueSet.toMutableList()
println("轉回 MutableList:$newList")
newList[0] = 10
newList.add(20)
println("修改後:$newList")
}
執行結果:
原始 List:[1, 1, 2, 2, 3, 4, 5, 5]
轉成 Set:[1, 2, 3, 4, 5]
轉回 MutableList:[1, 2, 3, 4, 5]
修改後:[10, 2, 3, 4, 5, 20]
這是實務上常見的清單去重方式,但必須注意:轉成 Set 後會失去重複次數資訊。參考資料:Kotlin Set-specific Operations
Kotlin 的 Set 提供 union()、intersect() 及 subtract(),分別代表聯集、交集與差集。參考資料:Kotlin Set-specific Operations
fun main() {
val group1 =
setOf(
"Mary",
"John",
"Nancy",
"Brown"
)
val group2 =
setOf(
"Leo",
"Mary",
"Brown",
"Joanna",
"Black"
)
// 聯集:兩組的所有元素。
val unionResult =
group1.union(group2)
// 交集:兩組共同擁有的元素。
val intersectResult =
group1.intersect(group2)
// 差集:group1 有,但 group2 沒有的元素。
val subtractResult =
group1.subtract(group2)
println(
"聯集:${unionResult.joinToString()}"
)
println(
"交集:${intersectResult.joinToString()}"
)
println(
"差集:${subtractResult.joinToString()}"
)
}
執行結果:
聯集:Mary, John, Nancy, Brown, Leo, Joanna, Black
交集:Mary, Brown
差集:John, Nancy
三種集合運算可以這樣理解:參考資料:Kotlin Set-specific Operations
| 函式 | 意義 | 範例用途 |
|---|---|---|
union() |
兩邊全部合併並去重 | 合併兩份會員名單 |
intersect() |
只保留雙方共同元素 | 找出共同好友 |
subtract() |
保留前者獨有元素 | 找出尚未參加活動者 |
Map 保存的是 Key 與 Value 的對應關係。每個 Key 必須唯一,但不同 Key 可以對應相同的 Value。參考資料:Kotlin Map-specific Operations
fun main() {
val student =
mutableMapOf(
"name" to "Mary",
"email" to "mary@gmail.com",
"phone" to "111111"
)
println(student)
println(
"姓名:${student["name"]}"
)
println(
"Email:${student["email"]}"
)
// 新增資料。
student["city"] = "桃園"
// 修改既有資料。
student["phone"] = "0912345678"
println(student)
}
執行結果:
{name=Mary, email=mary@gmail.com, phone=111111}
姓名:Mary
Email:mary@gmail.com
{name=Mary, email=mary@gmail.com, phone=0912345678, city=桃園}
如果需要保存多位學生,可以建立 MutableList<MutableMap<String, String>>。外層 List 代表多筆學生資料,內層 Map 則保存每位學生的姓名、Email 及電話。參考資料:Kotlin Nested Collections
fun main() {
val student1 =
mutableMapOf(
"name" to "Mary",
"email" to "mary@gmail.com",
"phone" to "111111"
)
val student2 =
mutableMapOf(
"name" to "John",
"email" to "john@gmail.com",
"phone" to "222222"
)
val student3 =
mutableMapOf(
"name" to "Jack",
"email" to "jack@gmail.com",
"phone" to "333333"
)
val students:
MutableList<MutableMap<String, String>> =
mutableListOf()
students.add(student1)
students.add(student2)
students.add(student3)
for (student in students) {
val name =
student["name"]
val email =
student["email"]
val phone =
student["phone"]
println(
"姓名:$name,Email:$email,電話:$phone"
)
}
}
執行結果:
姓名:Mary,Email:mary@gmail.com,電話:111111
姓名:John,Email:john@gmail.com,電話:222222
姓名:Jack,Email:jack@gmail.com,電話:333333
這種巢狀集合可以運作,但正式專案中如果資料欄位固定,通常使用 data class 會比 Map<String, Any> 更安全,也能避免 Key 拼錯或 Value 型別不明的問題。參考資料:Kotlin Data Classes
/**
* 學生資料模型。
*
* @property name 學生姓名。
* @property email 電子郵件。
* @property phone 電話號碼。
*/
data class Student(
val name: String,
val email: String,
val phone: String
)
fun main() {
val students =
mutableListOf(
Student(
name = "Mary",
email = "mary@gmail.com",
phone = "111111"
),
Student(
name = "John",
email = "john@gmail.com",
phone = "222222"
),
Student(
name = "Jack",
email = "jack@gmail.com",
phone = "333333"
)
)
students.forEach { student ->
println(
"姓名:${student.name}," +
"Email:${student.email}," +
"電話:${student.phone}"
)
}
}
二維陣列可以理解成「陣列裡面再放陣列」。以下範例中,第一層代表四間分店,第二層代表每間分店十二個月的營收。參考資料:Kotlin Nested Arrays
fun main() {
// 第一層代表分店,第二層代表 1~12 月。
val revenue =
arrayOf(
intArrayOf(
120, 135, 150, 140,
160, 180, 190, 210,
200, 195, 220, 250
),
intArrayOf(
100, 110, 130, 145,
155, 170, 185, 195,
205, 215, 225, 240
),
intArrayOf(
90, 105, 120, 130,
145, 160, 175, 185,
190, 200, 210, 230
),
intArrayOf(
110, 125, 140, 150,
165, 175, 180, 205,
210, 220, 235, 260
)
)
analyzeShopRevenue(revenue)
findMaximumRevenueShop(revenue)
analyzeMonthlyRevenue(revenue)
findMaximumRevenueMonth(revenue)
}
/**
* 計算每間分店的全年總營收及平均月營收。
*
* @param revenue 四間分店十二個月的營收資料。
*/
fun analyzeShopRevenue(
revenue: Array<IntArray>
) {
val shopNames =
arrayOf(
"A 店",
"B 店",
"C 店",
"D 店"
)
println("===== 分店全年營收 =====")
for (shopIndex in revenue.indices) {
var total = 0
for (
monthIndex in
revenue[shopIndex].indices
) {
total +=
revenue[shopIndex][monthIndex]
}
val average =
total.toDouble() /
revenue[shopIndex].size
println(
"${shopNames[shopIndex]}:" +
"全年 $total 萬元," +
"平均每月 ${
String.format(
"%.2f",
average
)
} 萬元"
)
}
println()
}
/**
* 找出全年總營收最高的分店。
*
* @param revenue 四間分店十二個月的營收資料。
*/
fun findMaximumRevenueShop(
revenue: Array<IntArray>
) {
val shopNames =
arrayOf(
"A 店",
"B 店",
"C 店",
"D 店"
)
var maximumShopIndex = 0
var maximumRevenue = Int.MIN_VALUE
for (shopIndex in revenue.indices) {
var total = 0
for (monthlyRevenue in revenue[shopIndex]) {
total += monthlyRevenue
}
if (total > maximumRevenue) {
maximumRevenue = total
maximumShopIndex = shopIndex
}
}
println("===== 最高營收分店 =====")
println(
"分店:${shopNames[maximumShopIndex]}"
)
println("全年營收:$maximumRevenue 萬元")
println()
}
/**
* 統計所有分店每個月的總營收。
*
* @param revenue 四間分店十二個月的營收資料。
*/
fun analyzeMonthlyRevenue(
revenue: Array<IntArray>
) {
println("===== 每月總營收 =====")
for (
monthIndex in
revenue.first().indices
) {
var monthlyTotal = 0
for (shopIndex in revenue.indices) {
monthlyTotal +=
revenue[shopIndex][monthIndex]
}
println(
"${monthIndex + 1} 月:" +
"$monthlyTotal 萬元"
)
}
println()
}
/**
* 找出全公司總營收最高的月份。
*
* @param revenue 四間分店十二個月的營收資料。
*/
fun findMaximumRevenueMonth(
revenue: Array<IntArray>
) {
var maximumMonthIndex = 0
var maximumRevenue = Int.MIN_VALUE
for (
monthIndex in
revenue.first().indices
) {
var monthlyTotal = 0
for (shopIndex in revenue.indices) {
monthlyTotal +=
revenue[shopIndex][monthIndex]
}
if (monthlyTotal > maximumRevenue) {
maximumRevenue = monthlyTotal
maximumMonthIndex = monthIndex
}
}
println("===== 最高營收月份 =====")
println(
"月份:${maximumMonthIndex + 1} 月"
)
println("總營收:$maximumRevenue 萬元")
}
資料結構沒有絕對的好壞,重點是資料的語意。如果資料數量固定,可以使用 Array;資料具有順序而且可能重複,使用 List;只需要唯一資料,使用 Set;需要透過識別名稱取得對應資料,使用 Map。參考資料:Kotlin Collections Overview
資料數量固定
→ Array
資料有順序、允許重複
→ List
資料不可重複
→ Set
資料以 Key 對應 Value
→ Map
以 Android App 為例,RecyclerView 的資料來源通常使用 List;已選取的商品編號可用 Set 防止重複;使用者編號與使用者資料的對應可用 Map;固定月份或固定感測器通道則可以使用 Array。參考資料:Android Kotlin Collections Codelab
今天認識了 Kotlin 中最重要的資料容器。Array 長度固定且能使用索引;List 有順序並允許重複;Set 保證元素唯一;Map 則使用 Key 對應 Value。參考資料:Kotlin Collections Overview
其中 Set 最適合用於去除重複資料、保存唯一編號、記錄已選取項目,以及進行聯集、交集與差集等集合運算。參考資料:Kotlin Set-specific Operations
今天最重要的語法整理如下:參考資料:Kotlin Constructing Collections
// 固定長度陣列。
val numbers =
intArrayOf(
11,
22,
33
)
// 唯讀 List。
val nameList =
listOf(
"Mary",
"John",
"Nancy"
)
// 可修改 List。
val mutableNames =
mutableListOf(
"Mary",
"John"
)
mutableNames.add("Nancy")
// 唯讀 Set,自動移除重複項目。
val uniqueNumbers =
setOf(
11,
22,
22,
33
)
// 可修改 Set。
val selectedIds =
mutableSetOf(
101,
102
)
selectedIds.add(103)
// List 轉 Set,移除重複元素。
val uniqueList =
listOf(
1,
1,
2,
2,
3
).toSet()
// Set 集合運算。
val group1 =
setOf(
"Mary",
"John"
)
val group2 =
setOf(
"Mary",
"Nancy"
)
val unionResult =
group1.union(group2)
val intersectResult =
group1.intersect(group2)
val subtractResult =
group1.subtract(group2)
// Map。
val student =
mutableMapOf(
"name" to "Mary",
"email" to "mary@gmail.com"
)
下一篇可以繼續介紹 Kotlin 的 Map 進階操作,並將集合資料搭配 data class,建立更接近 Android App 實務使用的學生資料管理系統。參考資料:Kotlin Map Operations