Kotlin 中的集合

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 事前準備

在這個程式碼研究室中,您將進一步瞭解 集合,以及 Kotlin 中的 lambdas 和高階函式。

必要條件

  • 對先前的程式碼研究室中提到的 Kotlin 概念有基本瞭解。
  • 熟悉如何使用 Kotlin Playground 來建立及編輯 Kotlin 程式。

課程內容

  • 如何使用集和圖等集合
  • 瞭解 lambdas 的基本概念
  • 高階函式的基本概念

軟硬體要求

2. 瞭解集合

集合是指一組相關項目,例如字詞清單或一組員工記錄。集合可以包含排序或未排序的項目,這些項目可以是獨一無二的或重複的。您已經瞭解一種類型的集合,那就是列表 列表中項目是有序的,但項目可以重複。

如同列表,Kotlin 可區分可變集合和不可變集合。Kotlin 提供多項功能,可用於新增或刪除項目、檢視及操控集合。

建立列表

在這項工作中,您會建立數字列表,然後加以排序。

  1. 開啟 Kotlin Playground
  2. 使用下列程式碼取代所有程式碼:
fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
}
  1. 如要執行程式,請輕觸綠色箭頭,然後查看畫面上顯示的結果:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
  1. 這份清單包含 0 到 9 這 10 個數字。某些數字會重複出現數次,而有些則一次也沒有出現。
  2. 在這個清單中,項目的順序很重要:第一個項目是 0,第二個項目是 3,依此類推。除非您變更順序,否則這些項目都會按照這個順序顯示。
  3. 回想一下,先前的程式碼研究室中提到過,列表有許多內建函式 (例如 sorted()函式,會傳回遞增排序后的列表)。在 println() 之後,為程式新增一行以列印排序後的列表:
println("sorted: ${numbers.sorted()}")
  1. 再次執行程式並查看結果:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]

將數字排序,方便您查看個別數字在列表中出現的次數,或者是完全沒有出現。

瞭解集

Kotlin 中的另一種集合類型為 。這是由相關項目組成的一個群組,但和列表不同,集中不允許出現重複的項目,但是項目之間是無序的。一個項目可以包含於一個集或不包含與一個集,只要位於組合中,則該項目僅能在該集中出現一次。這與數學中的集合概念類似。舉例來說,有一組您已讀的書籍。閲讀某本書多次並不影響您閱讀過的書籍這一集合。

  1. 將以下幾行指令新增至程式,即可將列表轉換成集:
val setOfNumbers = numbers.toSet()
println("set:    ${setOfNumbers}")
  1. 執行程式並查看結果:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]
set:    [0, 3, 8, 4, 5, 9, 2]

結果包含原始列表中的所有數字,但每個數字只會出現一次。請注意,這些數字的順序與原始清單中的順序相同,但這個順序對集來說無關緊要。

  1. 定義可變集合和不可變集合,並以相同的數字組初始化這些集,方法是新增下列幾行程式碼:
val set1 = setOf(1,2,3)
val set2 = mutableSetOf(3,2,1)
  1. 新增一行以列印二者是否相等:
println("$set1 == $set2: ${set1 == set2}")
  1. 執行您的程式並查看新的結果:
[1, 2, 3] == [3, 2, 1]: true

即使只有一個集是可變的,而且它們的項目順序不同,然而,系統仍將其視為相同的集,因為這二者包含完全相同的項目集。

您對集執行的主要作業之一是,透過 contains() 函式檢查特定項目是否包含於某個集中。您已經見過 contains()了,但是是使用在列表中。

  1. 如果集合中包含 7,新增如下一行以列印:
println("contains 7: ${setOfNumbers.contains(7)}")
  1. 執行您的程式並查看額外的結果:
contains 7: false

您也可以試著使用集合中的一個項目的值來測試。

    All of the code above:
fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
    println("sorted: ${numbers.sorted()}")
    val setOfNumbers = numbers.toSet()
    println("set:    ${setOfNumbers}")
    val set1 = setOf(1,2,3)
    val set2 = mutableSetOf(3,2,1)
    println("$set1 == $set2: ${set1 == set2}")
    println("contains 7: ${setOfNumbers.contains(7)}")
}

和數學中的集合一樣,在 Kotlin 中,您也可以運用 intersect()union() 對兩個集合執行交 (∩) 或 並 (∪) 運算。

瞭解 圖

在本程式碼研究室中,您要瞭解的最後一個集合類型是字典。圖是一組鍵/值組合,可讓您以特定鍵輕鬆查詢值。鍵不得重複,每個鍵只能對應至一個值,但值可以重複。圖中的值可以是字串、數字或物件,甚至是列表或一組集合等其他集合。

31796d892f69470d.png

如果您有成對的資料,圖就相當實用,您可以基於它的值辨別出每一對數據。該鍵會對應到相對應的值。

  1. 在 Kotlin playground 中,請將所有程式碼替換成這段程式碼,以建立可修改的圖來儲存使用者名稱和年齡:
fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    println(peopleAges)
}

這項操作會將 String (鍵) 的可變圖對應到 Int (值)、將兩個項目初始化,然後列印項目。

  1. 執行程式並查看結果:
{Fred=30, Ann=23}
  1. 如要在圖中加入更多項目,您可以使用 put() 函式傳入鍵和值:
peopleAges.put("Barbara", 42)
  1. 您也可以使用簡寫標記來新增項目:
peopleAges["Joe"] = 51

以下是上述所有程式碼:

fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    peopleAges.put("Barbara", 42)
    peopleAges["Joe"] = 51
    println(peopleAges)
}
  1. 執行程式並查看結果:
{Fred=30, Ann=23, Barbara=42, Joe=51}

如上所述,鍵 (姓名) 不得重複,但值 (年齡) 可以重複。當您嘗試使用同一個鍵新增項目時,會發生什麼情況?

  1. println() 之前加入這行程式碼:
peopleAges["Fred"] = 31
  1. 執行程式並查看結果:
{Fred=31, Ann=23, Barbara=42, Joe=51}

系統不會再次新增機碼 "Fred",但其所對應的值會更新為 31

如您所見,圖是在程式碼中快速映射鍵和值的方法!

3. 使用集合

雖然性質各不相同,但各種類型的集合有很多共同點。如果是可變集合,您可以新增或移除項目。您可以列舉集合中的所有項目、尋找特定項目,有時也可以將某種集合轉換成另一種集合。你先前曾執行過,透過 toSet()List 轉換成 Set。以下是使用集合的幾個實用函式。

forEach

假設您想列印「peopleAges」中的項目,並在其中納入使用者姓名和年齡。例如 "Fred is 31, Ann is 23,..."、等。您曾在先前的程式碼研究室中學到 for 這個迴圈,因此可以使用 for (people in peopleAges) { ... } 撰寫迴圈。

不過,列舉集合中的所有項目都是常見的作業,因此 Kotlin 提供的 forEach() 會分析所有項目,並在各個項目中執行。

  1. 在 playground 中,將下列程式碼加到 println() 後方:
peopleAges.forEach { print("${it.key} is ${it.value}, ") }

這與 for 迴圈類似,但有點複雜。forEach 會使用特殊 ID it 為目前的項目指定變數,

請注意,在呼叫 forEach() 方法時無需加上括號,只要以大括號 {} 傳送程式碼即可。

  1. 執行您的程式並查看額外的結果:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51,

這與您需要的非常接近,但結尾有一個額外的逗號。

將集合轉換為字串是一種常見的作業,而結尾的分隔符也是一個常見問題。我們將在下列步驟中說明相關處理方式。

地圖

map() 函式 (不應與上方地圖或字典集合混淆) 會將集合中的每個項目套用到轉換中的每個項目。

  1. 將程式中的 forEach 陳述式替換成這一行:
println(peopleAges.map { "${it.key} is ${it.value}" }.joinToString(", ") )
  1. 執行您的程式並查看額外的結果:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51

可產生正確的輸出內容,而且沒有多餘的逗號!有一行有太多工作,請密切關注。

  • peopleAges.map 會為 peopleAges 中的每個項目套用一項轉換,並建立新的轉換項目項目集合
  • 大括號 {} 中的部分會定義每個項目的轉換作業。轉換作業會使用鍵/值組合,並將其轉換為字串,例如 <Fred, 31> 會轉換為 Fred is 31
  • joinToString(", ") 會將轉換集合中的每個項目新增至字串中,並以 , 分隔,但不知道是否要將該項目新增到最後一個項目
  • 這兩者會與 . (點號運算子) 串連在一起,就像您先前在程式碼研究室中透過函式呼叫和資源存取作業一樣

過濾器

產品素材資源集合的另一個常見做法,就是尋找符合特定條件的項目。filter() 函式會根據運算式傳回相符集合中的項目。

  1. println() 之後,新增以下這行:
val filteredNames = peopleAges.filter { it.key.length < 4 }
println(filteredNames)

另請注意,filter 的呼叫不需要括號,而 it 是指清單中目前的項目。

  1. 執行您的程式並查看額外的結果:
{Ann=23, Joe=51}

在此情況下,運算式會取得鍵的長度 (String),並檢查其是否小於 4。任何符合條件的項目 (也就是名稱少於 4 個字元) 都會新增至新的集合。

將篩選器套用至對應時,系統會傳回新的類型 (LinkedHashMap)。您可以在對應上進行額外的處理,或將其轉換成其他類型的集合,例如清單。

4. 瞭解 lambda 和高階函式

Lambdas

上方程式碼中的模式看起來是否熟悉?

peopleAges.forEach { print("${it.key} is ${it.value}") }

有變數 (peopleAges) 調用了函式 (forEach)。在函式名稱前方加上括號時,你會在大括號後方看到大括號 {} 的部分程式碼。程式碼中也會使用上一個步驟中的 mapfilter 函式。forEach 函式會透過 peopleAges 變數調用,並使用大括號中的程式碼。

就好像您用大括號表示小函式,卻並沒有函式名稱。這個概念 (沒有名稱可立即做為運算式的函式) 是很實用的概念,也就是所謂的「lambda 運算式」,簡稱 lambda,

這是一個重要主題,可讓您瞭解如何利用 Kotlin 功能強大的函式與函式互動。您可將函式儲存在變數和類別中、將引數傳遞為引數,甚至傳回傳回函式。就像其他變數 (例如 IntString) 的變數一樣。

函式類型

為了啟用這種類型的行為,Kotlin 提供了一個「函式類型」,可讓您根據其輸入參數和傳回值定義特定類型的函式。格式如下:

函式類型範例:(Int) -> Int

具有上述函式類型的函式必須採用 Int 類型的參數,並傳回 Int 類型的值。在函式類型標記中,這些參數會以括號列出 (如有多個參數,請以半形逗號分隔)。旁邊有箭頭 ->,後面接著傳回類型。

哪些函式符合這項條件?如下所示,您可以使用 lambda 運算式將整數輸入的值改成三倍,如下所示。針對 lambda 運算式的語法,參數會先顯示 (以紅色方塊醒目顯示),後面接著函式箭頭,然後是函式內文 (以紫色方塊醒目顯示)。lambda 中的最後一個運算式是傳回值。

252712172e539fe2.png

您甚至可以將 lambda 儲存在變數中,如下圖所示。這個語法類似於宣告基本資料類型 (例如 Int) 的變數。觀察變數名稱 (黃色方塊)、變數類型 (藍色方塊) 和變數值 (綠色方塊)。triple 變數會儲存函式。其類型為 (Int) -> Int 的函式類型,且值為 lambda 運算式 { a: Int -> a * 3}

  1. 在遊樂場裡試用這個程式碼。你可以傳送 triple 函式 (如 5) 來呼叫並呼叫它。4d3f2be4f253af50.png
fun main() {
    val triple: (Int) -> Int = { a: Int -> a * 3 }
    println(triple(5))
}
  1. 產生的輸出內容應為:
15
  1. 在大括號中,可明確宣告參數 (a: Int)、省略函式箭頭 (->),而是只包含函式主體。更新 main 函式中宣告的 triple 函式,然後執行程式碼。
val triple: (Int) -> Int = { it * 3 }
  1. 輸出結果應該相同,但僅以更精簡的方式呈現 lambda!如需更多 lambda 的範例,請參考這個 資源
15

高階函式

您現在已學會在 Kotlin 中操縱函式的彈性了,接著我們要談談另一個功能強大的概念,也就是高階函式。這只是將函式 (在本例中為 lambda) 傳遞給其他函式,或從另一個函式傳回函式。

結果指出,mapfilterforEach 函式都是高階函式的範例,因為它們都會將函式視為參數。(在傳送至這個 filter 較高順序函式的 lambda 中,您可以省略單一參數和箭頭符號,也可以使用 it 參數)。

peopleAges.filter { it.key.length < 4 }

以下是高優先順序函式的範例:sortedWith()

如果想排序字串清單,可以使用內建的 sorted() 方法集合。不過,如果您想用字串長度為清單排序,就必須撰寫一些程式碼來比較兩個字串的長度。Kotlin 可讓您將 lambda 傳遞至 sortedWith() 方法,藉此達成此目標。

  1. 在遊樂場中,使用這個程式碼建立名稱清單,並按名稱排序輸出該清單:
fun main() {
    val peopleNames = listOf("Fred", "Ann", "Barbara", "Joe")
    println(peopleNames.sorted())
}
  1. 現在請將 lambda 傳送至 sortedWith() 函式,按名稱長度排序清單。lambda 應採用相同類型的兩個參數並傳回 Int。在 main() 函式的 println() 陳述式後方加上這行程式碼。
println(peopleNames.sortedWith { str1: String, str2: String -> str1.length - str2.length })
  1. 執行程式並查看結果。
[Ann, Barbara, Fred, Joe]
[Ann, Joe, Fred, Barbara]

傳送至 sortedWith() 的 lambda 有兩個參數,str1Stringstr2 則為 String。然後,您就會依次看到函式箭頭和函式主體。

7005f5b6bc466894.png

請記住,lambda 中的最後一個運算式是傳回值。在這種情況下,系統會傳回第一個字串和第二個字串長度之間的差異,也就是 Int。符合排序所需的項目:如果 str1 少於 str2,則會傳回小於 0 的值。如果 str1str2 的長度相同,系統會傳回 0。如果 str1 超過 str2,就會傳回大於 0 的值。透過一次比較兩個 Strings 的方式,sortedWith() 函式會輸出清單,清單將按照長度遞增排序。

Android 中的 OnClickListener 和 OnKeyListener

將這些與您到目前為止在 Android 系統上學到的知識結合起來,可以得知您之前在程式碼研究室中使用了 lambda,例如在 Tip Calculator 應用程式中為按鈕設定點擊事件監聽器時:

calculateButton.setOnClickListener{ calculateTip() }

只要使用 lambda 來設定點擊事件監聽器,就能快速完成。編寫上述程式碼的長版方法如下所示,並與精簡版相比。您不一定要知道長版版本的所有詳細資料,但需要注意這兩種版本的模式。

29760e0a3cac26a2.png

觀察 lambda 與 OnClickListener 中的 onClick() 方法具有相同函式類型的方式 (使用一個 View 引數並傳回 Unit,表示沒有傳回值)。

由於 Kotlin 中名為 SAM (Single-Abstract-Method) 轉換,您可以縮短程式碼的版本。Kotlin 會將 lambda 轉換為一個實作單一抽象方法 onClick()OnClickListener 物件。只要確認 lambda 函式類型與抽象函式的函式類型相符即可。

由於 lambda 中一律不使用 view 參數,因此可略過該參數。接著我們只具備 lambda 中的功能主體。

calculateButton.setOnClickListener { calculateTip() }

這些概念非常具有挑戰性,因此請耐心等候,因為這些概念需要時間與經驗,才能收 sink。再舉另一個例子當您在 Tip Calculator 的「Cost of service」文字欄位中設定鍵監聽器時,請注意,按下 Enter 鍵時,螢幕小鍵盤可能會隱藏。

costOfServiceEditText.setOnKeyListener { view, keyCode, event -> handleKeyEvent(view, keyCode) }

查詢 OnKeyListener 時,抽象方法擁有下列參數 onKey(View v, int keyCode, KeyEvent event),並傳回 Boolean。由於 Kotlin 中的 SAM 轉換,您可以將 lambda 傳送至 setOnKeyListener()。請確認 lambda 的函式類型為 (View, Int, KeyEvent) -> Boolean

以下是使用的 運算式。參數的值為 View、keyCode 和 event。函式主體包含使用 handleKeyEvent(view, keyCode) 並傳入 Boolean 的參數。

f73fe767b8950123.png

5. 製作字詞清單

現在,請將您學到的集合、lam 符號和高順序函式全部納入考量,然後應用在實際用途中。

假設您想要建立 Android 應用程式來玩文字遊戲或學習字彙。這個應用程式看起來會像這樣,每個字母中的每個字母都會顯示按鈕:

45d7aa76e7c2c20e.png

按一下字母 A,畫面上隨即會顯示一份以字母 A 開頭的幾個字詞完整清單等等。

你需要收集一組字詞,但要使用哪一種集合?如果應用程式要包含一些字母開頭的字母,您就必須找到或整理所有以特定字母開頭的字詞。如要增加挑戰,建議您在每次使用者執行應用程式時,從產品素材資源集合中選擇不同的字詞。

首先是字詞清單。建立誠實的應用程式時,您會想看一些較冗長的字詞,包括以字母表中所有字母做為開頭的字詞,但現在一個簡短的清單就足夠了。

  1. 使用下列程式碼取代 Kotlin 遊樂場中的程式碼:
fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
}
  1. 如要取得開頭為字母 B 的字詞集合,您可以使用 filter 搭配 lambda 運算式。新增以下幾行內容:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
println(filteredWords)

如果字串開頭為指定字串,startsWith() 函式則會傳回 true。您也可以指定系統忽略大小寫,因此「b」的比對結果會包括「b」或「B」。

  1. 執行程式並查看結果:
[balloon, best, brief]
  1. 別忘了,您希望應用程式隨機挑選字詞。使用 Kotlin 集合時,您可以使用 shuffled() 函式,為隨機隨機分組的項目建立副本。同時變更篩選過的字詞。
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
  1. 執行您的程式並查看新的結果:
[brief, balloon, best]

由於這類字詞會隨機隨機播放,所以您看到的字詞可能會有不同的排列順序。

  1. 您不想使用所有字 (尤其是實際字詞清單,尤其是幾個字)。您可以使用 take() 函式取得產品素材資源集合中的第一個項目。讓篩選後的字詞只包含前兩個重組字詞:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
  1. 執行您的程式並查看新的結果:
[brief, balloon]

因為隨機隨機的隨機調整功能,每次執行時系統可能會顯示不同的字詞。

  1. 最後,請針對該應用程式的隨機字詞清單隨機排序。一如往常,您可以使用 sorted() 函式,傳回含有下列項目的集合副本:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
    .sorted()
  1. 執行您的程式並查看新的結果:
[balloon, brief]

上述所有程式碼:

fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
    val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
        .shuffled()
        .take(2)
        .sorted()
    println(filteredWords)
}
  1. 建議您變更程式碼,建立一份內含開頭為 c 的隨機字詞清單。您必須對上述程式碼改變什麼?
val filteredWords = words.filter { it.startsWith("c", ignoreCase = true) }
    .shuffled()
    .take(1)

在實際應用程式中,您必須為每個字母系統套用篩選器,但現在已瞭解如何為每個字母產生字詞清單!

影片素材資源集合功能強大且富有彈性。學生有很多工作,我們有很多方法可以做。您越常瞭解程式設計,接下來將說明如何找出最適合哪種類型的資料收集方式,以及最佳處理方法。

使用 Lambda 和高階函式,可以更輕鬆地使用集合,並保持精簡。這些提案非常實用,方便您日後再次使用。

6. 總結

  • 集合是指一組相關項目
  • 集合可以是可變更或不可變更的
  • 集合可以是有序或無序的
  • 集合可以有唯一項目或允許重複項目
  • Kotlin 支援不同類型的集合,包括清單、集合和地圖
  • Kotlin 提供了許多處理及轉換集合的功能,包括 forEachmapfiltersorted 等。
  • lambda 是一種不含名稱的函式,不可用來立即傳遞運算式。例如:{ a: Int -> a * 3 }。
  • 高階函式指的是將函式傳送至其他函式,或傳回來自其他函式的函式。

7. 瞭解詳情