Kotlin 中的集合

1. 事前準備

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

必要條件

  • 對先前的程式碼研究室中提到的 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

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

上述所有程式碼:

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() 對兩個集合執行交 (∩) 或 並 (∪) 運算。

瞭解 圖

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

b55b9042a75c56c0.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. 在 Playground 中,使用這個程式碼建立名稱清單,並按名稱排序輸出該清單:
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,例如在小費計算機應用程式中為按鈕設定點按事件監聽器時:

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() }

這些概念非常具有挑戰性,因此請耐心累積經驗,您需要一些時間才能沉澱所學。再舉另一個例子回想一下您在小費計算機的「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 應用程式來玩文字遊戲或學習字彙。這個應用程式看起來會像這樣,每個字母中的每個字母都會顯示按鈕:

7539df92789fad47.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. 瞭解詳情