在 Collections 章節中,我們有提到 List
、Set
等集合用法,眼尖的朋友可能會發現到,在宣告一個新集合時,我們都必須使用 < >
和設定型態來進行宣告, 而這樣的方法其實就是一種泛型(Generic)應用,集合只是一個容器,為我們提供迭代元素、新增元素、刪除元素等操作方法,當我們需求上需要儲存什麼樣別的資料,再為這個容器加上型別即可使用。
可能有朋友會好奇,為什麼會需要使用泛型?假設今天我們有一個集合物件,裡面充滿著各種型別的資料,那我們在引用這個集合時,必然會在程式撰寫上處理許多型別轉型的工作,而型別轉型工作會讓我們程式多了一層轉換工作,所以為了減少不必要的轉型工作,我們可以提前在編譯期間(Compile Time)利用泛型告知此集合或方法屬於哪種型別,也是方便我們在開發時清楚要使用什麼類型的資料進行溝通,所以在型別安全檢查、程式碼品質、開發效率等都會帶來好處。
泛型可以讓我們使用在類別、介面、函數上,我們可以直接使用下面範例來觀察,我們定義一個使用泛型的Person 類別,再定義其他資料類別,
fun main() {
val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
val student: Person<Student> = Person(Student("Devin", "B991"))
println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}")
println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}")
// 印出以下結果:
// 老師姓名: Eric, 職員編號: A12345
// 學生姓名: Devin, 學號: B991
}
// 建立一個使用泛型物件的類別
class Person<T>(person: T) {
var data: T = person
}
// 建立一個老師類別,具有name、employeeNumber參數
class Teacher(val name: String, val employeeNumber: String)
// 建立一個學生類別,具有name、studentID參數
class Student(val name: String, val studentID: String)
泛型參數我們通常會利用字母 T(英文 Type)表示,若要使用其他名稱也可以,但在支援泛型的程式語言中大多使用 T 來表示,這樣可以讓其他開發者更容易了解我們的程式碼,而泛型還有其他常用的命名,如下:
泛型也允許使用多個泛型參數,參數名稱建議可參考上面常見規範,我們可以將上面的範例進行修改,在原本的 Person 類別中加入一個支援多種泛型的函數,如下範例:
fun main() {
val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
val student: Person<Student> = Person(Student("Devin", "B991"))
println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}")
println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}")
teacher.speak { println("${teacher.data.name}: 開始上課")}
student.speak { println("${student.data.name}: 老師好")}
}
// 建立一個使用泛型物件的類別
class Person<T>(person: T) {
var data: T = person
// 定義一個支援 lambda 運算式的函數,R為新增的泛型參數,作為函數的返回類型
fun <R> speak(func: (T) -> R): R? {
return func(data)
}
}
data class Teacher(val name: String, val employeeNumber: String)
data class Student(val name: String, val studentID: String)
上面範例我們都只使用一個資料進行操作,若我們想要一次使用多筆資料,此時可以使用 vararg
關鍵字,讓泛型類別可支援多個參數,參數即為元素陣列,而既然是陣列資料,我們就可以使用索引進行取值,我們可以搭配 get
運算函數進行索引取值動作,如下範例:
fun main() {
val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
val student: Person<Student> = Person(Student("Devin", "B991"), Student("Jack", "B992"))
println("老師姓名: ${teacher[0]?.name}, 職員編號: ${teacher[0]?.employeeNumber}")
println("學生姓名: ${student[0]?.name}, 學號: ${student[0]?.studentID}")
println("學生姓名: ${student[1]?.name}, 學號: ${student[1]?.studentID}")
teacher.speak(0) { println("${teacher[0]?.name}: 開始上課")}
student.speak(0) { println("${student[0]?.name}: 老師好")}
student.speak(0) { println("${student[1]?.name}: 老師好")}
}
// 建立一個使用泛型物件的類別
class Person<T>(vararg person: T) {
// 修改 data 資料型別為 Array<out T>, out 代表我們要將泛型 T 作為內部函數的返回值
var data: Array<out T> = person
operator fun get(index: Int): T? = data[index]
fun <R> speak(index: Int, func: (T) -> R): R? {
return func(data[index])
}
}
data class Teacher(val name: String, val employeeNumber: String)
data class Student(val name: String, val studentID: String)
在前一個範例中我們有用到 out
關鍵字,我們發現若在泛型類別中要將泛型用在內部函數的返回值上,必須加上 out
關鍵字,而 out
關鍵字其實有一個夥伴- in
關鍵字,in
則是將泛型用在函數參數值上。
而泛型參數其實扮演兩種角色:生產者(producer)
或 消費者(consumer)
,若身為生產者時,只能讀不能寫;消費者則相反,不能讀只能寫,而生產者為 out
關鍵字,消費者則為 in
關鍵字。
接下來,我們利用範例來觀察 in
& out
的實際狀況,首先介紹 out
關鍵字, out
泛型可以讓我們將子類別的泛型物件賦值給父類別泛型物件:
fun main() {
// 正常情況
// 食品商店屬於食品商店
val producer1 : Production<Food> = FoodStore()
// 速食商品屬於食品商店
val producer2 : Production<Food> = FastFoodStore()
// 漢堡商店也屬於食品商店
val producer3 : Production<Food> = InOutBurger()
// 錯誤情況
// 食品商店不見得屬於漢堡商店
// val producer1 : Production<Burger> = FoodStore()
// 速食商店也不見得屬於漢堡商店
// val producer2 : Production<Burger> = FastFoodStore()
// 漢堡商店屬於漢堡商店
// val producer3 : Production<Burger> = InOutBurger()
}
open class Food
open class FastFood : Food()
class Burger : FastFood()
// 定義一個生產者介面,運用 out 關鍵字
interface Production<out T> {
// 將泛型 T 作為回傳值
fun produce(): T
}
// 定義一個 FoodStore 類別,並利用 Food 類別實作生產者介面
class FoodStore : Production<Food> {
override fun produce(): Food {
println("食品商店")
return Food()
}
}
// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面
class FastFoodStore : Production<FastFood> {
override fun produce(): FastFood {
println("速食商店")
return FastFood()
}
}
// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面
class InOutBurger : Production<Burger> {
override fun produce(): Burger {
println("漢堡商店")
return Burger()
}
}
再來是 in
關鍵字的用法,in
泛型可以讓我們將父類別泛型物件賦值給子類別泛型物件,以下是範例:
fun main() {
// 正常情況
// 想購買肉品食物的消費者可能也會想買漢堡
val consumer1 : Consumer<Burger> = PurchaseFood()
// 想購買速食食物的消費者可能也會想買漢堡
val consumer2 : Consumer<Burger> = EatFastFood()
val consumer3 : Consumer<Burger> = EatBurger()
// 錯誤情況
// val consumer1 : Consumer<Food> = PurchaseFood()
// 直接想購買速食商品的消費者通常不會想買肉品
// val consumer2 : Consumer<Food> = EatFastFood()
// 直接想購買漢堡商品的消費者通常不見想買肉品
// val consumer3 : Consumer<Food> = EatBurger()
}
// 利用 in 關鍵字配合泛型
interface Consumer<in T> {
// 將泛型 T 作為函數參數
fun consume(item: T)
}
// 想購買食品商品
class PurchaseFood : Consumer<Food> {
override fun consume(item: Food) {
println("購買食品商品")
}
}
// 想購買速食食物
class EatFastFood : Consumer<FastFood> {
override fun consume(item: FastFood) {
println("購買速食食物")
}
}
// 想購買漢堡食物
class EatBurger : Consumer<Burger> {
override fun consume(item: Burger) {
println("購買漢堡食物")
}
}
【官方】Kotlin 官方文件
【文章】深入理解 Java 泛型