各位 Kotlin 爱好者们,早上好。
今天我们深入研究一下 Kotlin 泛型。
跟其他语言一样,Kotlin 通过泛型来处理不同的类型。你可以定义适用于不同类型的类、方法和属性,让代码更容易复用,同时保留类型安全。
我们可以把泛型理解成一个能装不同东西的盒子:水果、玩具、书都能装,不需要为每种东西单独设计一个盒子。泛型容器和函数也是如此,它们能处理不同类型的数据,还能在程序运行前帮你发现类型错误。
这篇文章从简单例子开始,逐步介绍 Kotlin 开发者需要了解的泛型基础。
各代码块按独立示例阅读,重复的类名、函数名和 main 不需要合并到同一个文件;健康动物示例会复用前一个示例中的 Animal。
什么是泛型
泛型允许类、接口、方法和属性使用稍后才确定的类型。需要处理多种数据类型,又希望保留类型安全时,就可以用它。
例如:你需要一个容器,能存放整数、字符串或自定义对象。与其为每种类型写一个类,不如用泛型定义一个可复用的容器类。
在 Kotlin 中,同一个泛型类、方法或属性可以处理不同类型的数据,减少重复代码。
基本语法
Kotlin 泛型类和泛型函数的基本写法如下:
class Box<T>(val content: T)
fun <T> printContent(content: T) {
println(content)
}
这里的 T 是类型参数,使用类或函数时,可以用具体类型替换它。
下面定义一个简单的泛型类 Box:
class Box<T>(val value: T) {
fun getContent(): T {
return value
}
}
fun main() {
val intBox = Box(10)
val stringBox = Box("Hello")
println(intBox.getContent()) // Output: 10
println(stringBox.getContent()) // Output: Hello
}
这个 Box 可以存放整数、字符串,也可以存放其他类型的内容。同一份类定义可以用于不同数据类型。
例如,购物车系统可以用这样的 Box 存放不同类型的商品,包括电子产品、书籍和食品。
泛型的好处
类型安全:编译器会检查类型,降低运行时出现 ClassCastException 的风险。
代码复用:用同一个类或函数处理不同类型,不需要重复实现。
类型转换与性能:泛型可以减少调用方手写的类型转换,但不能据此保证性能更好。JVM 泛型存在类型擦除,编译器仍可能插入类型转换,基本类型还可能涉及装箱。
可读性:相比直接使用 Any,泛型能更清楚地表达类型之间的关系和代码意图。
泛型函数
泛型也可以用于函数,让同一个函数处理不同类型的数据。
例如:你需要打印任意列表中的元素。整数列表、字符串列表等都可以交给同一个泛型函数,不必分别实现。
fun <T> printList(items: List<T>) {
for (item in items) {
println(item)
}
}
fun main() {
val intList = listOf(1, 2, 3)
val stringList = listOf("a", "b", "c")
println("Printing integer list:")
printList(intList) // Output: 1, 2, 3
println("Printing string list:")
printList(stringList) // Output: a, b, c
// Works with custom types too
data class User(val id: Int, val name: String)
val userList = listOf(
User(1, "Alice"),
User(2, "Bob"),
User(3, "Charlie")
)
println("Printing user list:")
printList(userList) // Output: User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Charlie)
}
printList 可以接收不同元素类型的列表并打印内容,包括自定义类型的列表。
进阶用法
泛型函数还可以实现转换和筛选等操作:
// Transform a list of one type to another type
fun <T, R> List<T>.transformList(transform: (T) -> R): List<R> {
return this.map { transform(it) }
}
// Find elements in a list that match a condition
fun <T> List<T>.findElements(predicate: (T) -> Boolean): List<T> {
return this.filter { predicate(it) }
}
fun main() {
val numberList = listOf(1, 2, 3, 4, 5)
// Square each number
val squaredNumbers = numberList.transformList { it * it }
println("Squared numbers: $squaredNumbers") // Output: [1, 4, 9, 16, 25]
// Find even numbers
val evenNumbers = numberList.findElements { it % 2 == 0 }
println("Even numbers: $evenNumbers") // Output: [2, 4]
// Works with any type
data class Product(val name: String, val price: Double)
val products = listOf(
Product("Laptop", 1200.0),
Product("Phone", 800.0),
Product("Headphones", 150.0)
)
// Transform products to their names
val productNames = products.transformList { it.name }
println("Product names: $productNames") // Output: [Laptop, Phone, Headphones]
// Find products under $500
val affordableProducts = products.findElements { it.price < 500.0 }
println("Affordable products: $affordableProducts") // Output: [Product(name=Headphones, price=150.0)]
}
默认值处理
Kotlin 允许函数参数声明默认值,泛型函数也一样。下面的例子展示的是两种提供回退值的方式:由调用方传入,或根据类型选择;它们没有在参数声明中使用 = 默认值。
使用场景:从数据库读取一个值,如果没有找到,就返回默认值。用泛型可以让这套逻辑适用于不同类型的数据。
提供回退值的泛型函数示例:
// Version 1: Explicit fallback value
fun <T> getValueOrDefault(value: T?, defaultValue: T): T {
return value ?: defaultValue
}
// Version 2: More advanced approach with type-specific defaults
inline fun <reified T> getValueOrDefault(value: T?): T {
return value ?: when (T::class) {
String::class -> "" as T
Int::class -> 0 as T
Boolean::class -> false as T
Double::class -> 0.0 as T
List::class -> emptyList<Any>() as T
Map::class -> emptyMap<Any, Any>() as T
else -> throw NotImplementedError("No default value for type ${T::class.simpleName}")
}
}
fun main() {
// Version 1: Using explicit defaults
println(getValueOrDefault("Hello", "Default")) // Output: Hello
println(getValueOrDefault(null, "Default")) // Output: Default
// Version 2: Using type-specific defaults (explicit type shown here)
println(getValueOrDefault<String>("Hello")) // Output: Hello
println(getValueOrDefault<String>(null)) // Output: "" (empty string)
println(getValueOrDefault<Int>(null)) // Output: 0
println(getValueOrDefault<Boolean>(null)) // Output: false
// Custom types with Version 1
data class User(val name: String)
val defaultUser = User("Anonymous")
println(getValueOrDefault(null, defaultUser)) // Output: User(name=Anonymous)
}
这个例子给出了两个版本:
版本一:调用方显式传入默认值,可以用于任意类型。
版本二:使用 reified 类型参数,根据类型自动选择默认值。
版本一每次调用都要传入默认值,例如 getValueOrDefault("Hello", "Default")。
版本二不需要传入默认值。示例显式指定了类型;如果编译器能从实参或上下文推断出 T,也可以省略类型实参。
你可以根据需要选择:显式提供默认值,或由函数根据类型选择默认值。第二种写法只支持 when 中列出的类型,其余类型在值为 null 时会抛出异常。
第二种写法只是有限类型的示例,不能当作通用默认值工厂。T::class 不包含 List<String> 等类型的元素类型信息,emptyList() 和 emptyMap() 返回的也是只读集合。Kotlin/JVM 上,可变与只读集合映射到相同的 Java 接口,仅判断 T::class 无法区分它们;例如请求 MutableList<Int> 的默认值时,这里的转换会失败。需要可变集合或其他类型时,应使用版本一,显式提供符合 T 的回退值。
这里要区分两个问题:
普通泛型函数可以接收一个 T 类型的回退值;参数默认值也可以是表达式或函数调用,并不要求必须是具体常量。
如果要在函数体中使用 T::class 判断类型,则需要 inline 和 reified。
不变性(Invariance)
不变性(Invariance)指的是:即使两个类型参数之间存在子类型关系,使用它们的同一个泛型类型也不会因此产生子类型关系。
Kotlin 自定义泛型类型默认是不变的。
例如:如果容器类型是不变的,那么存放字符串的容器就不能直接传给接收 Container<Any> 的函数。这种限制用于保证类型安全。
这里需要区分具体声明:Kotlin 的只读 List 声明了协变,List<String> 可以赋给 List<Any>;MutableList 则是不变的。
class Container<T>(val value: T)
fun main() {
val stringContainer: Container<String> = Container("Hello")
val anyContainer: Container<Any> = stringContainer // Error: Type mismatch
val anyContainerCorrect: Container<Any> = Container<Any>("Hello")
println(anyContainerCorrect.value) // Output: Hello
}
Container<String> 不是 Container<Any> 的子类型,这体现了自定义泛型类型默认的不变性。上面的错误赋值是故意保留的反例,运行其余代码前需要注释掉这一行。
型变(Variance)
Kotlin 使用 out、in 和星投影 * 来表达泛型的型变与投影。
型变是指:类型参数之间的子类型关系,如何影响包含这些参数的泛型类型之间的关系。
1. out:协变
协变会保留类型参数之间的子类型关系。例如,Apple 是 Fruit 的子类型,那么协变类型 Producer<Apple> 也是 Producer<Fruit> 的子类型。
Kotlin 用 out 声明这种关系。
例如,你有一个 Apple 列表,需要将它传给接收只读 List<Fruit> 的方法。
open class Fruit
class Apple : Fruit()
class FruitProducer<out T : Fruit>(private val fruit: T) {
fun produce(): T {
return fruit
}
}
fun main() {
val appleProducer: FruitProducer<Apple> = FruitProducer(Apple())
val fruitProducer: FruitProducer<Fruit> = appleProducer
println(fruitProducer.produce()) // Prints an Apple instance; exact text varies
}
FruitProducer<out T> 声明了协变,因此可以把 FruitProducer<Apple> 赋给 FruitProducer<Fruit>。
这个很好理解,对于生产者(向外产生值),假设我规定了只能给你水果(fruitProducer),那么无论我给你苹果(appleProducer),还是梨子,都是合理的。
处理集合和类型层级时,这种关系很有用。
2. in:逆变
逆变会反转类型参数之间的子类型关系。
例如,Apple 是 Fruit 的子类型,那么逆变类型 Consumer<Fruit> 是 Consumer<Apple> 的子类型(倒反天罡)。
Kotlin 用 in 声明这种关系,常用于消费输入值的类型。
例如:一个消费者能处理所有 Fruit,当然也能处理 Apple。
逆变允许你把这个 Fruit 消费者用在要求 Apple 消费者的位置。单纯给接收 Fruit 的函数传入 Apple,本身只需要普通的子类型关系。
open class Fruit
class Apple : Fruit()
class FruitConsumer<in T : Fruit> {
fun consume(fruit: T) {
println("Consuming a fruit")
}
}
fun main() {
val fruitConsumer: FruitConsumer<Fruit> = FruitConsumer() // FruitConsumer can consume any Fruit
val appleConsumer: FruitConsumer<Apple> = fruitConsumer // Contravariance allows this
appleConsumer.consume(Apple()) // Output: Consuming a fruit
}
FruitConsumer<in T> 允许把 FruitConsumer<Fruit> 赋给 FruitConsumer<Apple>。这样,能处理一般类型的消费者也可以用于只要求处理某个子类型的场景。
用上面这个例子来讲,我说我可以处理水果(fruitConsumer),那么肯定也能处理苹果呀(appleConsumer)!
如果还不好理解,可以想象一个 MutableList<Fruit>:既然能添加水果,那么也能添加苹果。你可以把它赋给 MutableList<in Apple>,通过这个引用添加 Apple,但读取元素时只能得到 Any?。
同时使用 out 和 in
下面通过接口,把协变和逆变放在同一个例子中:
例如,系统既需要生产不同类型的对象,也需要消费它们:工厂生产 Fruit,商店消费这些对象。
open class Fruit
class Apple : Fruit()
interface Source<out T> {
fun next(): T
}
interface Sink<in T> {
fun accept(item: T)
}
fun <T> feedSink(item: T, sink: Sink<T>) {
sink.accept(item)
}
fun main() {
val stringSource: Source<String> = object : Source<String> {
override fun next(): String = "Kotlin"
}
val anySource: Source<Any> = stringSource
println(anySource.next()) // Output: Kotlin
val fruitSink: Sink<Fruit> = object : Sink<Fruit> {
override fun accept(item: Fruit) {
println("Consuming a fruit")
}
}
val appleSink: Sink<Apple> = fruitSink
feedSink(Apple(), appleSink)
}
在这个例子中:
Source<out T> 是协变的,因此 Source<String> 可以赋给 Source<Any>。
Sink<in T> 是逆变的,因此 Sink<Fruit> 可以赋给 Sink<Apple>。
Kotlin 中,生产者常用协变,消费者常用逆变。可以这样记:
类只返回(生产)T 类型的值时,使用 out。
类只接收(消费)T 类型的值时,使用 in。
类既生产又消费 T 类型的值时,通常不声明型变。
这个记法与 Java 中的 PECS 原则相对应。不过,Java 的通配符用于使用处,前面的 Kotlin 接口是在声明处指定型变:
Producer Extends, Consumer Super
- Producer(生产者)→ Extends:如果泛型对象主要用于生产(提供)数据,使用
? extends T。 - Consumer(消费者)→ Super:如果泛型对象主要用于消费(接收)数据,使用
? super T。
理解这些关系,有助于设计类型安全、方便调用的 API,尤其是在处理集合和类型层级时。
理解 where 子句
where 可以为类型参数设置多个约束,也就是要求一个类型同时具备几种能力。
where 子句是什么
假设你需要一个类型,它既是 Animal,又能参与比较,以便找出年龄最大的动物。where 可以同时表达这两个要求。
下面的函数从列表中找出年龄最大的动物。比较年龄时使用 compareTo,避免原文直接相减可能出现的整数溢出:
// Base animal class with age
open class Animal(val age: Int)
// Dog class that extends Animal and can be compared
class Dog(age: Int) : Animal(age), Comparable<Dog> {
override fun compareTo(other: Dog): Int {
return this.age.compareTo(other.age)
}
}
// Function to find the oldest animal
// The "where" clause requires T to be BOTH:
// 1. A type of Animal AND
// 2. Something that can be compared
fun <T> findOldestAnimal(animals: List<T>): T where T : Animal, T : Comparable<T> {
return animals.maxByOrNull { it.age } ?: throw IllegalArgumentException("List is empty")
}
fun main() {
val dogs = listOf(Dog(5), Dog(3), Dog(9), Dog(7))
val oldestDog = findOldestAnimal(dogs)
println("The oldest dog is ${oldestDog.age} years old.") // Output: The oldest dog is 9 years old.
}
接下来的 Healthy 接口会增加 healthScore 属性,用来判断动物是否健康。
HealthyDog 会继承 Animal,实现用于年龄比较的 Comparable<HealthyDog>,并实现提供健康评分的 Healthy。
findOldestHealthyAnimal 会通过 where 要求 T 同时满足 Animal、Comparable<T> 和 Healthy 三个约束,以便先按健康评分筛选,再找出年龄最大的动物。
例如:找出年龄最大的健康动物
// Health interface with a health score
interface Healthy {
val healthScore: Int
}
// HealthyDog that is an Animal, can be compared, and has health info
class HealthyDog(age: Int, override val healthScore: Int) : Animal(age), Comparable<HealthyDog>, Healthy {
override fun compareTo(other: HealthyDog): Int {
return this.age.compareTo(other.age)
}
}
// Function requiring THREE conditions:
// 1. Must be an Animal AND
// 2. Must be comparable AND
// 3. Must have health information
fun <T> findOldestHealthyAnimal(animals: List<T>): T where T : Animal, T : Comparable<T>, T : Healthy {
// Only consider animals with good health
return animals.filter { it.healthScore > 50 }
.maxByOrNull { it.age } ?: throw IllegalArgumentException("No healthy animals found")
}
fun main() {
val healthyDogs = listOf(
HealthyDog(5, 70),
HealthyDog(3, 80),
HealthyDog(9, 60),
HealthyDog(7, 90)
)
val oldestHealthyDog = findOldestHealthyAnimal(healthyDogs)
println("The oldest healthy dog is ${oldestHealthyDog.age} years old with a health score of ${oldestHealthyDog.healthScore}.")
// Output: The oldest healthy dog is 9 years old with a health score of 60.
}
Healthy 接口提供 healthScore 属性,作为判断动物健康状况的依据。
HealthyDog 继承 Animal,实现 Comparable<HealthyDog> 来比较年龄,同时实现 Healthy 来提供健康评分。
findOldestHealthyAnimal 通过 where 要求 T 同时满足这三个约束,然后筛选健康评分大于 50 的动物,再找出年龄最大的那个。这里按 age 调用 maxByOrNull,并没有使用 compareTo;Comparable<T> 在这两个示例中是演示多重约束,并非函数实现所必需。
星投影
不知道确切的类型实参时,可以使用星投影 *,在保留类型安全的前提下处理未知类型。
例如:处理集合或 API 时,具体类型实参未知,或者当前操作不需要关心它,就可以使用星投影。
fun printList(list: List<*>) {
for (item in list) {
println(item)
}
}
fun main() {
val intList: List<Int> = listOf(1, 2, 3)
val stringList: List<String> = listOf("a", "b", "c")
printList(intList) // Output: 1, 2, 3
printList(stringList) // Output: a, b, c
}
List<*> 表示元素类型未知的列表,可以接收不同元素类型的列表。只需要处理集合,而不关心元素的确切类型时,这种写法很方便。
什么时候使用星投影
星投影适合以下场景:
不需要类型信息:当前操作不依赖元素的具体类型。
与旧代码交互:对接使用原始类型或通配符的 Java 代码。
处理异构集合:集合中可能包含多种类型的元素。
如果你觉得 List<*> 和 List<Any> 差不多,需要先区分可空性:List<*> 读出的元素是 Any?,可能为 null;List<Any> 的元素则是非空的。对只读 List 而言,List<*> 与 List<Any?> 的读取能力相同;下面用 MutableList 看星投影对写入的限制。
星投影的限制
星投影也会限制你能执行的操作:
fun addToList(list: MutableList<*>, item: Any) {
// This doesn't compile!
// list.add(item)
// We cannot add an element of an unknown type; clear() is still allowed
println("The list contains ${list.size} items")
}
对于上面的 MutableList<*>,通常可以:
读取元素,得到 Any? 类型的值。
调用返回 Any? 的方法。
调用不依赖具体元素类型的方法。
但不能:
通过 add 或 set 写入元素,因为不知道列表实际接受什么类型。
调用要求你提供未知元素类型值的方法。
星投影并不意味着集合不可修改,例如仍可调用 clear()。对其他带上界的泛型类型,读取结果应按其上界理解,不一定都是 Any?。
更复杂的例子
// Function to count elements that match a specific condition
fun countElements(collection: Collection<*>, predicate: (Any?) -> Boolean): Int {
var count = 0
for (item in collection) {
if (predicate(item)) {
count++
}
}
return count
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val strings = listOf("apple", "banana", "cherry", "date")
// Count even numbers
val evenCount = countElements(numbers) { it is Int && it % 2 == 0 }
println("Number of even integers: $evenCount") // Output: 2
// Count strings starting with 'a'
val aCount = countElements(strings) { it is String && it.startsWith('a') }
println("Number of strings starting with 'a': $aCount") // Output: 1
}
这个计数函数通过星投影接收不同类型的集合,再用运行时类型检查(is)为每个元素应用条件。这里把谓词参数写成 Any?,直接传入元素,避免原文的 item!! 在遇到 null 时抛出异常。
星投影让你可以处理不同类型的数据,同时保留对未知类型所需的安全限制。
泛型约束
有时,你需要限制泛型类或函数允许使用哪些类型。可以为类型参数定义上界,让传入的类型必须满足相应要求。
使用场景:一个数学函数只适用于数值类型。可以用泛型约束,确保调用方传入 Int、Float、Double 等合适的类型。
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(sum(3, 4)) // Output: 7.0
println(sum(3.5, 4.2)) // Output: 7.7
// println(sum("3", "4")) // Error: Type inference failed
}
T : Number 将 T 限定为 Number 的子类型,因此 sum 只能接收数值类型,不能传入字符串等类型。结果统一转为 Double,大整数转换和浮点运算可能损失精度,这个函数并不保证精确求和。
加上这个约束后:
可以使用 Int、Double、Float、Long 等类型。
不能使用 String、Boolean 等非数值类型。
函数体中可以调用 Number 的方法,例如 toDouble()。
如果使用 where,可以为同一个类型参数指定多个约束。
例如:某个排序算法要求元素既能比较,又能克隆。多个约束可以确保只有同时满足这些要求的类型才能传入。
fun <T> performOperation(item: T) where T : Comparable<T>, T : Cloneable {
// Compare the item with itself (just as an example)
if (item.compareTo(item) == 0) {
println("Item is equal to itself")
}
// Cloneable.clone() is protected, so this function cannot call it directly
}
fun main() {
// This would work if String implemented Cloneable (it doesn't in reality)
// performOperation("Hello")
// For demonstration, let's create a class that meets both constraints
class ComparableAndCloneable(val value: Int) : Comparable<ComparableAndCloneable>, Cloneable {
override fun compareTo(other: ComparableAndCloneable): Int {
return value.compareTo(other.value)
}
public override fun clone(): Any {
return ComparableAndCloneable(value)
}
}
performOperation(ComparableAndCloneable(10)) // Works because it satisfies both constraints
}
这里的 T 必须同时满足 Comparable<T> 和 Cloneable,因此 performOperation 只接受符合这两个条件的类型。
注意,Kotlin/JVM 的 Cloneable.clone() 是受保护成员,仅有这个上界不能让普通泛型函数直接调用它;强制转换成 Java 的 Cloneable 也不能解决这个问题。示例类通过公开重写 clone() 提供可调用的实现,但在泛型函数中还需要能公开表达该操作的接口或具体类型。
常见的约束类型
类约束:<T : SomeClass>。
接口约束:<T : SomeInterface>。
多个约束:<T> where T : Interface1, T : Interface2。
可空类型约束:<T : Any?> 允许可空类型,而 <T : Any> 只允许非空类型。不写上界时,默认上界就是 Any?。
泛型约束可以保留泛型的复用能力,同时让允许的类型和可执行的操作更明确。
为泛型类型参数设置多个上界
where 可以为同一个泛型类型参数设置多个上界,让它同时满足多个接口或父类约束。
使用场景:构建一个模拟系统,对象需要同时具备移动、绘制等行为。多个上界可以确保只接收具备这些能力的对象。
interface Vehicle {
fun drive()
}
interface Electric {
fun chargeBattery()
}
class Tesla : Vehicle, Electric {
override fun drive() {
println("Driving Tesla")
}
override fun chargeBattery() {
println("Charging Tesla's battery")
}
}
fun <T> operateVehicle(vehicle: T) where T : Vehicle, T : Electric {
vehicle.drive()
vehicle.chargeBattery()
}
fun main() {
val tesla = Tesla()
operateVehicle(tesla) // Output: Driving Tesla \n Charging Tesla's battery
// This would cause a compile-time error:
class GasCar : Vehicle {
override fun drive() {
println("Driving gas car")
}
}
// operateVehicle(GasCar()) // Error: Type argument is not within bounds
}
operateVehicle 只接受同时实现 Vehicle 和 Electric 的类型。通过 where 指定这两个上界,函数就能调用驾驶和充电操作。
例如:同时约束类和接口。
abstract class Machine {
abstract fun turnOn()
abstract fun turnOff()
}
interface Programmable {
fun program(task: String)
}
class Robot : Machine(), Programmable {
override fun turnOn() {
println("Robot powering on")
}
override fun turnOff() {
println("Robot shutting down")
}
override fun program(task: String) {
println("Robot programmed to: $task")
}
}
fun <T> operateMachine(machine: T) where T : Machine, T : Programmable {
machine.turnOn()
machine.program("Perform assembly tasks")
machine.turnOff()
}
fun main() {
val robot = Robot()
operateMachine(robot)
// Output:
// Robot powering on
// Robot programmed to: Perform assembly tasks
// Robot shutting down
}
总结
泛型初看可能有些绕,但它能让代码复用于不同类型,并保留类型检查。本文介绍了 Kotlin 泛型的基础。后面,我会再发一篇文章,讲解更多的实战案例。