泛型(Generics) 讓函數和類別能夠在保有型別安全的前提下,操作不同的資料型別,避免重複撰寫僅資料型別不同的程式碼。
1. 為什麼需要泛型#
假設我們需要一個「容器」類別,如果不使用泛型,就必須為每種型別各寫一個:
1
2
| class IntBox(val value: Int)
class StringBox(val value: String)
|
使用泛型,只需一個類別就能處理所有型別:
1
| class Box<T>(val value: T)
|
2. 泛型類別#
使用角括號 <T> 宣告型別參數,T 只是慣例命名,可以使用任意名稱。
1
2
3
4
5
6
7
8
9
10
11
| class Box<T>(val value: T) {
fun getValue(): T = value
}
fun main() {
val intBox = Box(42)
val strBox = Box("Hello")
println(intBox.getValue()) // 輸出:42
println(strBox.getValue()) // 輸出:Hello
}
|
多個型別參數#
1
2
3
4
5
6
7
8
| class Pair<A, B>(val first: A, val second: B) {
override fun toString() = "($first, $second)"
}
fun main() {
val pair = Pair("Alice", 30)
println(pair) // 輸出:(Alice, 30)
}
|
3. 泛型函數#
函數同樣可以宣告型別參數。
1
2
3
4
5
6
7
8
9
| fun <T> printItem(item: T) {
println("內容:$item,型別:${item!!::class.simpleName}")
}
fun main() {
printItem(42) // 輸出:內容:42,型別:Int
printItem("Kotlin") // 輸出:內容:Kotlin,型別:String
printItem(3.14) // 輸出:內容:3.14,型別:Double
}
|
泛型函數的實際應用:交換兩個元素#
1
2
3
4
5
6
7
8
9
10
11
| fun <T> swap(list: MutableList<T>, i: Int, j: Int) {
val temp = list[i]
list[i] = list[j]
list[j] = temp
}
fun main() {
val nums = mutableListOf(1, 2, 3, 4, 5)
swap(nums, 0, 4)
println(nums) // 輸出:[5, 2, 3, 4, 1]
}
|
4. 型別上界(Upper Bound)#
問題:沒有限制時能做什麼?#
沒有上界的泛型,編譯器只知道 T 是「某個型別」,無法呼叫任何特定方法。以下程式碼會編譯失敗:
1
2
3
| fun <T> max(a: T, b: T): T {
return if (a > b) a else b // 錯誤:T 不一定支援 > 運算子
}
|
解法:用 : 指定上界#
在 <T : 上界型別> 中指定上界,代表 T 必須是該型別或其子類別,編譯器就能確保 T 擁有該型別的所有方法。
1
2
3
4
5
6
7
8
9
| fun <T : Comparable<T>> max(a: T, b: T): T {
return if (a > b) a else b // 合法:Comparable<T> 提供了 > 運算子
}
fun main() {
println(max(3, 7)) // 輸出:7
println(max("apple", "banana")) // 輸出:banana
println(max(3.14, 2.71)) // 輸出:3.14
}
|
Comparable<T> 是 Kotlin / Java 標準函式庫的介面,Int、String、Double 等型別都有實作它,所以這三種呼叫都合法。
上界阻擋不相容的型別#
假設自訂一個沒有實作 Comparable 的類別,傳入 max() 時編譯器會直接報錯:
1
2
3
4
5
6
| class Point(val x: Int, val y: Int) // 未實作 Comparable
fun main() {
// 錯誤:Point 不符合 Comparable<Point> 上界
// max(Point(1, 2), Point(3, 4))
}
|
這正是上界的用意:在編譯期就攔截不合理的型別,而非等到執行時才崩潰。
上界實際應用:限制為數字型別#
1
2
3
4
5
6
7
8
9
| fun <T : Number> sum(list: List<T>): Double {
return list.sumOf { it.toDouble() }
}
fun main() {
println(sum(listOf(1, 2, 3))) // 輸出:6.0
println(sum(listOf(1.5, 2.5, 3.0))) // 輸出:7.0
// sum(listOf("a", "b")) // 錯誤:String 不是 Number
}
|
預設上界#
沒有指定上界時,預設上界為 Any?(可為 null 的任意型別),代表幾乎沒有限制。
多個上界:where 子句#
當需要同時滿足多個條件時,用 where 子句列出,角括號只能寫一個上界。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| interface Printable {
fun printInfo()
}
// T 必須同時是 Comparable<T> 且實作 Printable
fun <T> printMax(a: T, b: T) where T : Comparable<T>, T : Printable {
val bigger = if (a > b) a else b
bigger.printInfo()
}
data class Score(val value: Int) : Comparable<Score>, Printable {
override fun compareTo(other: Score) = value.compareTo(other.value)
override fun printInfo() = println("分數:$value")
}
fun main() {
printMax(Score(80), Score(95)) // 輸出:分數:95
}
|
5. 變異(Variance)#
變異描述了泛型型別與其型別參數之間的繼承關係。
協變(Covariant):out#
使用 out 修飾符表示型別參數只會被 輸出(回傳),不會被輸入。
1
2
3
4
5
6
7
8
9
10
| class Producer<out T>(private val value: T) {
fun produce(): T = value
}
fun main() {
val intProducer: Producer<Int> = Producer(42)
val anyProducer: Producer<Any> = intProducer // 合法,因為 out
println(anyProducer.produce()) // 輸出:42
}
|
Kotlin 標準函式庫中的 List<out E> 就是協變的,所以 List<Int> 可以賦值給 List<Any>。
逆變(Contravariant):in#
使用 in 修飾符表示型別參數只會被 輸入(接收),不會被輸出。
1
2
3
4
5
6
7
8
9
10
11
12
| class Consumer<in T> {
fun consume(value: T) {
println("消費:$value")
}
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer()
val intConsumer: Consumer<Int> = anyConsumer // 合法,因為 in
intConsumer.consume(42) // 輸出:消費:42
}
|
6. 星號投影(Star Projection)#
問題:List<Any> 並不萬用#
直覺上會想用 List<Any> 接受任何 List,但這樣行不通:
1
2
3
4
5
6
7
8
| fun printList(list: List<Any>) {
for (item in list) println(item)
}
fun main() {
val ints: List<Int> = listOf(1, 2, 3)
printList(ints) // 錯誤:List<Int> 無法傳給 List<Any>
}
|
因為 List<Int> 和 List<Any> 在 Kotlin 中是 不同型別,即使 Int 是 Any 的子類別。
解法:List<*> 星號投影#
* 代表「某個確定但未知的型別」,等同於 List<out Any?>。它告訴編譯器:「這個 List 裡裝著某種型別的元素,我不確定是什麼,但我只會讀,不會寫。」
1
2
3
4
5
6
7
8
9
| fun printList(list: List<*>) {
for (item in list) println(item)
}
fun main() {
printList(listOf(1, 2, 3)) // 輸出:1 2 3
printList(listOf("a", "b", "c")) // 輸出:a b c
printList(listOf(true, 3.14, "x")) // 混合型別也可以
}
|
星號投影只能讀,不能寫#
這是最重要的限制。因為編譯器不知道 * 實際是什麼型別,所以禁止任何寫入操作,防止型別不安全:
1
2
3
4
5
6
7
8
| fun main() {
val list: MutableList<*> = mutableListOf(1, 2, 3)
println(list[0]) // 合法:讀取,得到 Any?
list.add(4) // 錯誤:不能寫入,編譯器不知道 * 是什麼型別
list[0] = 99 // 錯誤:同上
}
|
星號投影 vs. Any vs. 泛型 T 比較#
| 寫法 | 意義 | 可讀 | 可寫 |
|---|
List<Any> | 明確裝 Any 的 List | ✅ | ✅ |
List<*> | 裝某個未知型別的 List | ✅(得到 Any?) | ❌ |
List<T> | 裝已知型別 T 的 List | ✅ | ✅ |
實際用途:只關心容器本身,不關心元素型別#
當函數只需要知道集合的 長度、是否為空、列印內容,而完全不需要操作元素型別時,* 是最適合的選擇。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| fun describeList(list: List<*>) {
println("元素數量:${list.size}")
println("是否為空:${list.isEmpty()}")
println("第一個元素:${list.firstOrNull()}")
}
fun main() {
describeList(listOf(10, 20, 30))
// 輸出:
// 元素數量:3
// 是否為空:false
// 第一個元素:10
describeList(emptyList<String>())
// 輸出:
// 元素數量:0
// 是否為空:true
// 第一個元素:null
}
|
7. 實際應用:泛型 Repository#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class Repository<T> {
private val items = mutableListOf<T>()
fun add(item: T) {
items.add(item)
}
fun getAll(): List<T> = items.toList()
fun findFirst(predicate: (T) -> Boolean): T? {
return items.firstOrNull(predicate)
}
}
data class User(val id: Int, val name: String)
fun main() {
val repo = Repository<User>()
repo.add(User(1, "Alice"))
repo.add(User(2, "Bob"))
repo.add(User(3, "Carol"))
val found = repo.findFirst { it.id == 2 }
println(found) // 輸出:User(id=2, name=Bob)
println(repo.getAll())
// 輸出:[User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Carol)]
}
|
Reference#
https://kotlinlang.org/docs/generics.html