iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
Software Development

Kotlin Lambda 從零開始系列 第 1

Kotlin Lambda 從零開始 Day 01:系列導讀 — 為什麼要手刻 Collection API?

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260805/201219483tq6P4KMBY.jpg

以前寫 C# 時,曾經上過 91 的 C# 進階設計課程。那時候印象很深刻的,是用重構和測試把 LINQ 相關的 API,例如 WhereSelect 等這類函式慢慢長出來,而不是停在只會使用 LINQ API

原本每天都在用的 LINQ,自己實作一次之後,才會看到它背後其實串著幾個基礎概念:extension method、delegate、lambda、generic、IEnumerable。API 看起來很短,但它把語法、型別設計和資料流都包在一起。這種方式很適合我,因為它不會只停在「這個 API 怎麼用」,還會逼自己去想「為什麼可以這樣設計」

後來學 Kotlin 時,我一直有類似的感覺。employees.map { it.name } 很像 C# 的 employees.Select(x => x.Name)filter 也很像 Where。可是寫久一點就會發現,兩邊只是長得像,細節不完全一樣

Kotlin 的 Collection API 預設是 eager,真的要 lazy pipeline 要切到 Sequence。Kotlin 的 Extension Function 寫法也跟 C# extension method 不同。再加上 inline、trailing lambda、function type、receiver 這些語法,熟悉 C# Lambda 不代表就真的理解 Kotlin Lambda

所以這個系列想做的事,就是把當年練習 C# LINQ 的方法搬到 Kotlin,自己手刻一組 Kotlin Collection API,從 filtermapfold 開始,一路碰到 Lambda、Extension Function、泛型、Sequence、Scope Functions、型變和 Contract

這樣做不是為了重寫標準函式庫。標準函式庫已經很好用,也不需要我們自己做一套。重點是藉由重做 API,把那些平常藏在一行 chain call 裡的概念拆開來看

系列會怎麼寫

這個系列會用 36 篇文章,從 Lambda 的本質一路寫到 Sequence、Scope Functions、泛型型變和 Contract

系列目錄

  • 第一部分・基礎篇
    • day 01 導讀
    • day 02 FunctionN 與閉包
    • day 03 擴充函式與泛型
    • day 04 inline
  • 第二部分・篩選與尋找篇
    • day 05 filter
    • day 06 first / last / single
    • day 07 any / all / none / count
    • day 08 find / indexOf
    • day 09 contains / elementAt
  • 第三部分・轉換篇
    • day 10 map
    • day 11 flatMap
    • day 12 associate
    • day 13 zip / unzip
    • day 14 groupBy
  • 第四部分・排序篇
    • day 15 sorted
    • day 16 Comparator
  • 第五部分・聚合篇
    • day 17 fold
    • day 18 reduce
    • day 19 groupingBy / Grouping
    • day 20 數值聚合
    • day 21 joinToString
  • 第六部分・切片篇
    • day 22 take / drop
    • day 23 takeWhile / dropWhile
    • day 24 chunked / windowed
  • 第七部分・集合運算篇
    • day 25 distinct
    • day 26 集合運算
  • 第八部分・Sequence 篇
    • day 27 Eager 與 Lazy
    • day 28 基礎
    • day 29 filter / map
    • day 30 中間與終端操作
    • day 31 效能與選擇
  • 第九部分・進階語法篇
    • day 32 Scope Functions
    • day 33 operator / infix
    • day 34 型變與 reified
    • day 35 Contract
  • 總結篇
    • day 36 系列回顧

主軸很單純:用 Kotlin 實作一組 myFiltermyMapmyFoldmyGroupBy 之類的函式。名字前面加 my,避免跟標準函式庫撞名,也提醒自己這是學習用的版本,不是要取代 stdlib

平常我們可能會直接寫

employees
    .filter { it.department == "Engineering" }
    .map { it.name }

這種寫法很順,但它把很多細節都藏起來了。自己實作一次後,會發現不少函式本體只是 for 迴圈加上一點條件或轉換,重點是函式的簽名

  • Lambda 怎麼表示一段行為
  • Extension Function 怎麼把函式掛到既有型別上
  • 泛型怎麼讓同一個函式支援不同資料型別
  • inline 為什麼會影響 Lambda 成本
  • Sequence 為什麼可以把 eager pipeline 改成 lazy pipeline

這也是我覺得手刻 Collection API 很適合拿來學 Kotlin Lambda 的原因。它夠小,容易測;但一路寫下去,又會自然碰到 Kotlin 進階語法

TDD 節奏怎麼安排

這個系列是用 TDD 的方式寫的,不過不會走到 baby step 那麼細。如果每篇都照著「寫一個測試 → 讓它通過 → 重構 → 再寫下一個測試」一輪一輪推進,文章會變得非常長,真正想講的 Kotlin 語法反而會被流程本身蓋過去

所以折衷了一下,每篇先把測試案例集中放在前面,讓你一次看完這個函式該有的行為和邊界條件,接著才是實作的程式碼

這只是文章的呈現順序,不是實際寫程式的順序。想自己練習的話,可以把前面那組測試每個都拆開拆細,一次只寫一個,先讓它紅燈,再讓它變綠,跑起來的感覺跟看文章不太一樣

這篇先把環境準備好

day 01 專心處理環境,後續文章會沿用同一個專案與測試節奏,不用再回頭處理 build 設定

這篇做三件事

  • 說清楚這個系列為什麼存在
  • 建立一個後續文章能沿用的 Kotlin JVM 專案
  • 準備一組每篇都會用到的共用測試資料

工具上我會改用 Kotlin Toolchain CLI。以前開 Kotlin JVM 專案通常會先碰 Gradle,對這個系列來說有點太重。這次只是要一個乾淨、能 build、能 run、能跑 test 的小專案,用 Kotlin Toolchain 的 jvm-cli template 就夠了

用 Kotlin Toolchain 建立專案

Kotlin Toolchain CLI 的做法,跟我在 Kotlin Toolchain:JetBrains 想為 Kotlin 打造一套更完整的工具生態系 這篇提到的一樣

我這次實測的版本是

kotlin --version

輸出是

Kotlin Toolchain version 0.11.0 (35ef359, 2026-05-20)

Kotlin Toolchain 目前仍在 Alpha 階段,這點要先注意。用它做這個系列的範例專案很適合,因為我們需要的是一個乾淨、簡單、能跑測試的 JVM console project,不是複雜的 production build

Kotlin Toolchain 版本和 Kotlin compiler 版本是兩件事。這個系列的 JVM 底層說明以 Kotlin 2.4.0、預設 Lambda 產生模式 invokedynamic 為基準;Toolchain CLI 則沿用下面實測的 0.11.0

這裡要特別提醒:Toolchain 0.11.0 預設帶的 compiler 不是 2.4.0,是 2.3.x。想確認手上實際跑的是哪一版,可以去看 CLI 快取目錄裡的 stdlib jar 檔名,或直接檢查產出 class 的 @Metadata。要對齊 2.4.0 得自己在 module.yaml 指定 compiler 版本

好消息是本系列用到的底層行為(invokedynamic 產生策略、inline 展開、型別擦除)在 2.3 和 2.4 之間沒有變化,所以就算你手上是 2.3.x,文章裡的結論一樣成立。只有要重跑 bytecode 或 benchmark 時,才需要先確認實際版本,不要只看 CLI 版本

建立 JVM console project

kotlin init 可以互動式選 template,也可以直接指定 template。官方文件把這個選項稱為「JVM console application」;在這次實測的 Toolchain 0.11.0 裡,CLI 使用的 template ID 是 jvm-cli

mkdir KotlinLambdaSample
kotlin init jvm-cli --target-dir=KotlinLambdaSample
cd KotlinLambdaSample

--target-dir 指向的目錄要先存在,所以前面的 mkdir 一定要先跑,不然會有問題

成功後會看到類似這樣的訊息

Extracting template jvm-cli to KotlinLambdaSample...
Project successfully generated

Now you may build your project with ./kotlin build or open this folder in an IDE with the Kotlin Toolchain plugin

注意最後建議的是 ./kotlin build,不是全域的 kotlin build。這個 ./kotlin 是專案內的 wrapper script,概念跟 Gradle Wrapper 很像:專案自己帶著固定入口,新成員 clone 下來後不用先手動裝同一版 CLI

產出的專案結構

jvm-cli template 產出的結構很小

KotlinLambdaSample/
├── kotlin
├── kotlin.bat
├── module.yaml
├── src/
│   ├── main.kt
│   └── World.kt
└── test/
    └── WorldTest.kt

module.yaml 只有一行

product: jvm/app

這表示它是一個 JVM application。對這個系列來說夠用了,後面要加自己的 Kotlin 檔案和測試,都可以直接放在這個專案裡

src/main.kt 預設長這樣

fun main() {
    println("Hello, ${World.get()}!")
}

src/World.kt 則是一個簡單的 object

object World {
    fun get(): String {
        return "World";
    }
}

這些只是 template 附的範例,後續可以保留、刪掉,或換成自己的檔案。對我們來說,重點是專案骨架已經有了,而且測試工具也接好了

確認 build 和 run

先跑 build

./kotlin build

實測輸出會看到它分別編譯正式程式碼和測試程式碼

00:01.249 INFO  :KotlinLambdaSample:compileJvm Compiling module 'KotlinLambdaSample' for platform 'jvm'...
00:02.911 INFO  :KotlinLambdaSample:compileJvmTest Compiling module 'KotlinLambdaSample' for platform 'jvm'...
Build successful

再跑程式

./kotlin run

會印出

Hello, World!
00:00.991 INFO  :KotlinLambdaSample:runJvm Process exited with exit code 0

第一行是程式輸出,第二行是 Kotlin Toolchain 的執行資訊。exit code 0 表示程式正常結束

處理 template 內建的失敗測試

jvm-cli template 會在 test/WorldTest.kt 放兩個測試,其中一個是故意失敗的 shouldFail()

@Test
fun shouldFail() {
    assertTrue(false)
}

所以你直接跑

./kotlin test

會看到 shouldFail() 失敗,這不是你環境壞掉,是 template 故意讓你看到測試失敗時的輸出

這個系列不需要保留它,把 shouldFail() 和沒用到的 assertTrue import 刪掉即可。保留 doTest() 後再跑一次

./kotlin test

測試就會通過

00:00.676 INFO  :KotlinLambdaSample:compileJvmTest Compiling module 'KotlinLambdaSample' for platform 'jvm'...
00:02.470 INFO  :KotlinLambdaSample:testJvm Testing module 'KotlinLambdaSample' for platform 'jvm'...
Started WorldTest
Started doTest()
Passed doTest()
Completed WorldTest

Test run finished after 82 ms
[         4 containers found      ]
[         0 containers skipped    ]
[         4 containers started    ]
[         0 containers aborted    ]
[         4 containers successful ]
[         0 containers failed     ]
[         1 tests found           ]
[         0 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

到這裡,我們有一個可以 build、run、test 的 Kotlin JVM 專案。後面的文章就不用再處理環境問題,可以直接把焦點放在 Collection API 和 Lambda

這個系列會用到的共用資料

後面大多數範例會圍繞同一組員工資料。把以下內容放進 src/Employee.kt,後續文章的測試就能直接共用

data class Employee(
    val id: Int,
    val name: String,
    val department: String,
    val salary: Int,
    val age: Int,
)

val employees = listOf(
    Employee(1, "Alice", "Engineering", 85_000, 30),
    Employee(2, "Bob", "Engineering", 72_000, 25),
    Employee(3, "Charlie", "Marketing", 65_000, 35),
    Employee(4, "Diana", "Marketing", 78_000, 28),
    Employee(5, "Eve", "HR", 60_000, 32),
    Employee(6, "Frank", "HR", 55_000, 40),
    Employee(7, "Grace", "Engineering", 92_000, 45),
)

固定測試資料有個好處:讀者不用每篇重新理解資料長什麼樣,可以把注意力放在函式本身

小結

環境的部分用 Kotlin Toolchain 的 jvm-cli template 解決了,後面 35 篇都在同一個專案裡加檔案,不用再處理 build 相關的問題

下一篇會進入 Lambda 本質,看看 Kotlin 的 Lambda 編譯後到底變成什麼,FunctionN 介面又扮演什麼角色,順便用 myForEach 把整個系列的 TDD 節奏跑第一次

參考資料


Yes


同步刊登於 Blog

圖片來源:AI 產生


系列文
Kotlin Lambda 從零開始1
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言