
上一篇介绍了 Kotlin 泛型的基础。
这一篇,我们先看类型参数之间的关系与约束,再把它们用于数据容器、结果包装、对象构建和数据转换,最后讨论运行时类型检查与反射的边界。
我想借这篇文章,把自己了解的一些泛型用法尽量整理出来,因此例子会多一些,读起来可能稍显啰嗦。受限于个人经验和理解,内容也未必周全,大家可以按需挑选感兴趣的章节阅读。
除特别说明外,各段示例独立阅读和运行,同名的 User、Person 等类型不需要放在同一个文件中。
多个类型参数
Kotlin 的类和函数都可以声明多个类型参数,用来表达不同数据之间的类型关系。
例如,一对数据的两个值可以具有不同类型。下面用一个简化的 Pair 和交换函数演示:
kotlin
class Pair<A, B>(val first: A, val second: B)
fun <A, B> swap(pair: Pair<A, B>): Pair<B, A> {
return Pair(pair.second, pair.first)
}
fun main() {
val pair = Pair(1, "one")
val swappedPair = swap(pair)
println(swappedPair.first) // Output: one
println(swappedPair.second) // Output: 1
}
Pair 声明了 A、B 两个类型参数。swap 返回一个新的 Pair,交换两个值的位置,对应的类型也从 Pair<A, B> 变成了 Pair<B, A>。
标准库已经提供了 Pair 和 Triple,分别承载两个和三个值,实际使用时不必重复定义。
递归类型上界
有时,类型参数的上界中还需要引用这个类型参数本身,这就是递归类型上界。
例如,我们要表达的不只是"T 可以比较",而是"T 可以与另一个 T 比较",所以约束 T : Comparable<T> 中又出现了 T 本身。
下面的比较工具用这个约束,确保能调用元素的 compareTo:
kotlin
class NumberComparable<T : Comparable<T>>(private val value: T) {
fun compare(other: T): Int {
return value.compareTo(other)
}
}
fun main() {
val number1 = NumberComparable(10)
println(number1.compare(20)) // Output: -1
}
这里使用 Kotlin 标准库的 Comparable,T : Comparable<T> 要求 T 是 Comparable<T> 的子类型,因此 T 类型的对象可以通过 compareTo 与同类型的对象比较。
组合类型参数与约束
前面分别介绍了多个类型参数和递归类型上界。把它们结合起来,就能实现一个按键排序的字典:值类型 V 不受限制,键类型 K 则必须支持比较。
kotlin
class Dictionary<K : Comparable<K>, V>(private val map: MutableMap<K, V> = mutableMapOf()) {
fun put(key: K, value: V) {
map[key] = value
}
fun get(key: K): V? {
return map[key]
}
fun sortedKeys(): List<K> {
return map.keys.sorted()
}
}
fun main() {
val phonebook = Dictionary<String, String>()
phonebook.put("Alice", "555-1234")
phonebook.put("Bob", "555-5678")
phonebook.put("Charlie", "555-9012")
println("Sorted contacts: ${phonebook.sortedKeys()}")
println("Bob's number: ${phonebook.get("Bob")}")
}
这里仅要求键 K 支持比较,值 V 不参与排序,因此不需要同样的约束。
泛型扩展函数
扩展函数也可以使用泛型。如果某个操作需要用于不同类型的对象,就可以把它写成泛型扩展函数。
例如,给 MutableList<T> 添加交换元素的功能,就不必为每种元素类型分别实现:
kotlin
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
fun main() {
val list = mutableListOf(1, 2, 3, 4)
list.swap(0, 2)
println(list) // Output: [3, 2, 1, 4]
}
swap 是 MutableList<T> 的扩展函数,可以用于不同元素类型的可变列表。
获取列表中间元素也是同样的思路,函数本身可以简写为:
kotlin
fun <T> List<T>.middle(): T? = getOrNull(size / 2)
列表为空时返回 null;元素个数为偶数时,返回中间两个元素中靠后的那个。
如果扩展函数需要调用元素的特定能力,还可以增加上界。例如,T : Number 让函数体可以调用 toDouble():
kotlin
fun <T : Number> List<T>.average(): Double {
if (isEmpty()) return 0.0
var sum = 0.0
for (item in this) {
sum += item.toDouble()
}
return sum / size
}
fun main() {
val integers = listOf(1, 2, 3, 4, 5)
val doubles = listOf(1.5, 2.5, 3.5)
println("Integer average: ${integers.average()}") // Output: 3.0
println("Double average: ${doubles.average()}") // Output: 2.5
}
上面的 average 是自定义扩展,空列表返回 0.0;标准库的 average() 在空列表上返回 NaN,两者行为不同。
泛型与数据结构
栈、队列、树等数据结构经常使用泛型,让同一份实现可以存放不同类型的数据。
以栈为例,T 关联了入栈参数与出栈返回值的类型:
kotlin
class Stack<T> {
private val elements = mutableListOf<T>()
fun push(item: T) {
elements.add(item)
}
fun pop(): T? {
if (elements.isNotEmpty()) {
return elements.removeAt(elements.size - 1)
}
return null
}
fun isEmpty(): Boolean {
return elements.isEmpty()
}
fun peek(): T? {
return elements.lastOrNull()
}
}
fun main() {
val stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
println(stack.pop()) // Output: 3
println(stack.peek()) // Output: 2
}
这个 Stack<T> 可以用于整数、字符串或自定义对象,只需要在使用时确定元素类型。
队列也可以使用相同的泛型设计,但出队和查看操作都从队首取元素。这里不再展开实现。
示例的出栈和查看操作在没有元素时返回 null。如果 T 本身允许 null,仅凭返回值就无法区分"容器为空"和"元素就是 null"。
泛型与委托
泛型容器关联了存入和取出的类型。同样的关系也可以用接口表达,再通过 Kotlin 的委托把接口实现交给另一个对象。
例如,用 Repository<T> 关联仓库的存入和读取类型,再通过 by 把接口实现转交给已有仓库:
kotlin
interface Repository<T> {
fun getAll(): List<T>
fun getById(id: String): T?
fun save(item: T): Boolean
}
class DatabaseRepository<T>(private val items: MutableList<T>,
private val idExtractor: (T) -> String) : Repository<T> {
override fun getAll(): List<T> = items
override fun getById(id: String): T? {
return items.find { idExtractor(it) == id }
}
override fun save(item: T): Boolean {
items.add(item)
return true
}
}
class RepositoryManager<T>(repository: Repository<T>) : Repository<T> by repository
data class User(val id: String, val name: String)
fun main() {
val userRepository = DatabaseRepository(
mutableListOf(User("1", "Alice"), User("2", "Bob")),
{ it.id }
)
val manager = RepositoryManager(userRepository)
println(manager.getById("2")) // Output: User(id=2, name=Bob)
manager.save(User("3", "Charlie"))
println(manager.getAll())
// Output: [User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Charlie)]
}
RepositoryManager<T> 中的 by repository 让编译器生成接口方法的转发实现,管理器无需手写这些方法。T 则让管理器与被委托的仓库保持相同的数据类型:这里传入 Repository<User>,读出的就是 User,保存时也需要传入 User。
泛型与数据类

Kotlin 的数据类也可以声明类型参数,用同一种结构承载不同类型的数据。
例如,API 的状态和错误字段保持一致,只有返回的数据类型不同,可以用 Response<T> 表达:
kotlin
data class Response<T>(val status: String, val data: T?, val error: String?)
fun main() {
val successResponse = Response("success", listOf(1, 2, 3), null)
val errorResponse = Response("error", null, "Something went wrong")
println(successResponse) // Output: Response(status=success, data=[1, 2, 3], error=null)
println(errorResponse) // Output: Response(status=error, data=null, error=Something went wrong)
}
Response<T> 统一了字段,但这些字段仍能任意组合,例如 status 为成功时,data 仍可能为 null,甚至同时带有错误信息。
如果要表达成功、失败和加载中这几种互斥状态,可以进一步使用密封类,让每种状态只携带所需的数据:
kotlin
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val code: Int) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun fetchUser(id: String): Result<User> {
// Simulating an API call
return if (id == "123") {
Result.Success(User("John Doe", "john@example.com"))
} else {
Result.Error("User not found", 404)
}
}
data class User(val name: String, val email: String)
fun main() {
val result = fetchUser("123")
when (result) {
is Result.Success -> {
val user = result.data
println("User found: ${user.name}, ${user.email}")
}
is Result.Error -> {
println("Error: ${result.message} (Code: ${result.code})")
}
is Result.Loading -> {
println("Loading...")
}
}
// Output: User found: John Doe, john@example.com
}
Success<T> 携带成功数据,Error 和 Loading 不需要成功数据,因此继承 Result<Nothing>。
Nothing 是所有类型的子类型;Result 通过 out T 声明为协变后,Result<Nothing> 也可以用于需要 Result<User> 的位置。处理结果时,when 根据状态访问对应的数据。
使用类型别名简化泛型类型
泛型嵌套较多时,类型声明可能变得很长。可以用类型别名给它起一个更容易理解的名字。
例如,一个按用户分组的映射,可以写成:
kotlin
typealias UserMap<T> = Map<String, List<T>>
fun <T> printUserMap(userMap: UserMap<T>) {
for ((key, value) in userMap) {
println("Key: $key, Value: $value")
}
}
fun main() {
val userMap: UserMap<String> = mapOf(
"admins" to listOf("Alice", "Bob"),
"users" to listOf("Charlie", "Dave")
)
printUserMap(userMap)
}
UserMap<T> 是 Map<String, List<T>> 的别名,可以让使用这个类型的代码更简洁,也更方便维护。类型别名不会创建新类型,别名与原类型可以互换使用。
函数类型也可以起别名,例如:
kotlin
typealias Processor<T, R> = (T) -> R
val doubler: Processor<Int, Int> = { it * 2 }
泛型与建造者模式

前面的例子主要关注数据如何存储和读取。创建对象时,也可以用泛型确定最终返回的类型。
建造者模式(Builder)常用于分步骤创建复杂对象。同一套建造逻辑可以用于不同商品,而每个建造者的返回类型在创建时就已经确定。
例如,笔记本和手机都具有名称、价格,又有各自特有的属性。可以把公共的设置步骤放进建造者,通过工厂函数决定最终创建哪种商品:
kotlin
open class Product(val name: String, val price: Double)
class ProductBuilder<T : Product>(private val factory: (String, Double) -> T) {
private lateinit var name: String
private var price: Double = 0.0
fun setName(name: String): ProductBuilder<T> {
this.name = name
return this
}
fun setPrice(price: Double): ProductBuilder<T> {
this.price = price
return this
}
fun build(): T {
return factory(name, price)
}
}
// Subclass example
class Laptop(name: String, price: Double, val cpuModel: String = "Intel i5") : Product(name, price)
class Smartphone(name: String, price: Double, val screenSize: Double = 6.1) : Product(name, price)
fun main() {
// For base Product
val productBuilder = ProductBuilder<Product> { name, price -> Product(name, price) }
val product = productBuilder.setName("Generic Product").setPrice(99.99).build()
println("Product: ${product.name}, Price: ${product.price}")
// For Laptop subclass
val laptopBuilder = ProductBuilder<Laptop> { name, price -> Laptop(name, price) }
val laptop = laptopBuilder.setName("Gaming Laptop").setPrice(1299.99).build()
println("Laptop: ${laptop.name}, Price: ${laptop.price}, CPU: ${laptop.cpuModel}")
// For Smartphone subclass
val phoneBuilder = ProductBuilder<Smartphone> { name, price -> Smartphone(name, price, 6.7) }
val phone = phoneBuilder.setName("Flagship Phone").setPrice(899.99).build()
println("Phone: ${phone.name}, Price: ${phone.price}, Screen: ${phone.screenSize}\"")
}
ProductBuilder 使用类型参数 T,并通过 T : Product 将它限定为 Product 的子类型。构造时传入一个工厂函数,build() 再调用这个函数,创建并返回 T 类型的对象。
因此,ProductBuilder<Laptop> 的 build() 返回 Laptop,调用方可以直接访问 cpuModel;增加商品类型时,只需提供对应的工厂函数。
示例的 name 使用 lateinit,调用 build() 前需要先调用 setName(),否则会抛出 UninitializedPropertyAccessException。
泛型与高阶函数
建造者中的工厂函数已经展示了如何通过函数参数指定创建逻辑。更一般地,高阶函数可以接收函数作为参数,也可以返回函数;配合泛型,还能表达数据转换前后的类型关系。
先看一次转换:输入是 T,处理函数把它转换成 R。
kotlin
fun <T, R> applyOperation(value: T, operation: (T) -> R): R {
return operation(value)
}
fun main() {
val result = applyOperation(5) { it * 2 }
println(result) // Output: 10
val stringLength = applyOperation("Kotlin") { it.length }
println(stringLength) // Output: 6
}
一次转换确定了输入与输出的关系;连续转换时,还需要把上一步的输出类型传给下一步。可以让每次操作返回一个承载新类型的 Pipeline<R>:
kotlin
class Pipeline<T> {
private var current: T
constructor(initialValue: T) {
current = initialValue
}
fun <R> pipe(transform: (T) -> R): Pipeline<R> {
return Pipeline(transform(current))
}
fun get(): T = current
}
fun main() {
val result = Pipeline("Hello")
.pipe { it.length } // String -> Int
.pipe { it * 2 } // Int -> Int
.pipe { "Result: $it" } // Int -> String
.get()
println(result) // Output: Result: 10
}
这里的类型沿调用链从 String 变成 Int,最后又变回 String。pipe 的返回类型记录了每一步的转换结果,下一步的参数类型也随之确定。
具体化类型参数
前面的类型关系主要由编译器检查。有时,我们还需要在运行时判断一个值是否属于类型参数指定的类型。
普通泛型存在类型擦除,不能直接用 is T 进行检查。Kotlin 提供了 reified 类型参数来处理这类需求,但它只能用于内联函数。
例如,检查一个值是否属于指定类型:
kotlin
inline fun <reified T> isOfType(value: Any): Boolean {
return value is T
}
fun main() {
println(isOfType<String>("Hello")) // Output: true
println(isOfType<Int>("Hello")) // Output: false
}
这里的 reified 让函数体可以使用 T 进行运行时类型检查,例如 value is T。按类型筛选集合元素或进行类型转换时,这种写法很方便。
使用 reified 按类型筛选列表
例如:一个列表里混合存放了多种类型的对象,现在只想取出其中某一种类型。可以这样写:
kotlin
inline fun <reified T> filterByType(items: List<Any>): List<T> {
return items.filterIsInstance<T>()
}
fun main() {
val mixedList: List<Any> = listOf(1, "Hello", 3.5, "World", 2)
val stringList: List<String> = filterByType(mixedList)
println(stringList) // Output: [Hello, World]
val intList: List<Int> = filterByType(mixedList)
println(intList) // Output: [1, 2]
}
filterByType 按指定的类型 T 筛选列表,把这套逻辑封装成可复用的工具函数,同时保留类型检查。
reified 的进阶用法
获取类引用:
kotlin
inline fun <reified T> getClassOf(): Class<T> {
return T::class.java
}
fun main() {
val stringClass = getClassOf<String>()
println(stringClass.simpleName) // Output: String
val intClass = getClassOf<Int>()
println(intClass.simpleName) // Output: Integer
}
拿到类引用后,还可以进行反射操作,后文会展示属性读取和实例创建。
使用具体类型进行 JSON 反序列化:
kotlin
import com.google.gson.Gson
inline fun <reified T> fromJson(json: String): T {
return Gson().fromJson(json, T::class.java)
}
data class User(val name: String, val email: String)
fun main() {
val jsonString = """{"name":"John","email":"john@example.com"}"""
val user = fromJson<User>(jsonString)
println("User: ${user.name}, ${user.email}") // Output: User: John, john@example.com
}
这里的 T::class.java 适用于示例中的 User 等非泛型目标类,无法保留 List<User> 这样的完整类型实参。反序列化泛型类型时,应使用 Gson 的 TypeToken 传递完整类型信息。另外,Gson 不保证遵守 Kotlin 的非空类型约束,不能仅凭返回类型就认定反序列化结果一定有效。
使用限制
- 只能用于内联函数:只有
inline函数的类型参数才能声明为reified。 - 不能用于类的类型参数:无法直接声明一个带
reified类型参数的泛型类。 - 不能递归调用:Kotlin 编译器不支持递归内联函数。
- 可能增加代码体积:内联会将函数体展开到调用处,多处调用可能使生成的代码变大。
reified 让函数能在运行时使用调用处确定的类型信息,支持普通泛型无法直接完成的类型检查和类引用等操作。不过,value is T 仍不能检查泛型对象内部已擦除的类型实参,例如不能据此确认列表中的元素都是 String。
泛型与单例对象
取得类引用后,还可以用它来区分共享实例。Kotlin 的单例 object 不能直接声明类型参数,不过可以用泛型类配合伴生对象,实现按类区分的共享存储。
例如,希望字符串和整数分别使用各自的共享存储,可以用类引用作为键,在伴生对象中缓存实例:
kotlin
class SingletonStore<T> private constructor() {
private val items = mutableListOf<T>()
fun addItem(item: T) {
items.add(item)
}
fun getAllItems(): List<T> {
return items
}
companion object {
private val instances = mutableMapOf<Class<*>, SingletonStore<*>>()
@Suppress("UNCHECKED_CAST")
fun <T> getInstance(clazz: Class<T>): SingletonStore<T> {
return instances.getOrPut(clazz) { SingletonStore<T>() } as SingletonStore<T>
}
}
}
fun main() {
val stringStore = SingletonStore.getInstance(String::class.java)
stringStore.addItem("First")
stringStore.addItem("Second")
println(stringStore.getAllItems()) // Output: [First, Second]
val intStore = SingletonStore.getInstance(Int::class.java)
intStore.addItem(3)
intStore.addItem(4)
println(intStore.getAllItems()) // Output: [3, 4]
}
若想省去调用时的类参数,只需在上面的伴生对象中增加一个重载,不必重写存储逻辑:
kotlin
inline fun <reified T> getInstance(): SingletonStore<T> =
getInstance(T::class.java)
这样就能写成 SingletonStore.getInstance<String>()。缓存仍保持私有,内联入口只调用公开的 getInstance(clazz)。
这里共享的是同一个 Class 对应的实例,不能区分 List<String> 和 List<Int>。示例没有处理并发访问;未检查的转换依赖缓存中类与实例的对应关系。
泛型与反射
前面的 reified 示例解决了如何取得类引用。取得引用后,还可以借助 Kotlin 反射查找属性、调用构造函数。下面重点看这些操作本身及其限制。
通过反射更安全地读取属性
按名称查找属性、读取值,再检查返回类型,就能在类型不匹配时避免直接强制转换:
kotlin
import kotlin.reflect.full.memberProperties
// Check the returned value before returning it to the caller
inline fun <T : Any, reified R> T.getPropertySafely(propertyName: String): R? {
val property = this::class.memberProperties.find { it.name == propertyName } ?: return null
val value = property.getter.call(this)
return if (value is R) {
value
} else {
println("Type mismatch: Property '$propertyName' is not of the requested type")
null
}
}
data class Person(val name: String, val age: Int, val email: String)
fun main() {
val person = Person("Alice", 30, "alice@example.com")
// Correct usage
val name: String? = person.getPropertySafely("name")
println("Name: $name") // Output: Name: Alice
// The method handles type mismatches gracefully
val ageAsString: String? = person.getPropertySafely("age")
println("Age as String: $ageAsString") // Output: Age as String: null (with type mismatch warning)
}
这个函数在属性不存在或返回值类型不匹配时返回 null;属性访问权限不足或 getter 自身抛出异常时,异常仍会向外传播。
这里的 value is R 同样不能检查泛型对象内部已擦除的类型实参,因此只能提供这一步类型检查,不能保证任意泛型属性都能安全读取。
通过反射创建实例
创建泛型类型的实例时,可以捕获构造调用中的异常,并在失败时返回 null:
kotlin
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
fun <T : Any> createInstance(clazz: KClass<T>, vararg args: Any?): T? {
val constructor = clazz.primaryConstructor ?: return null
return try {
constructor.call(*args)
} catch (e: Exception) {
println("Failed to create instance: ${e.message}")
null
}
}
class Person(val name: String, val age: Int)
fun main() {
val person = createInstance(Person::class, "John", 25)
if (person != null) {
println("Created person: ${person.name}, ${person.age}")
// Output: Created person: John, 25
}
// This will fail gracefully
val invalidPerson = createInstance(Person::class, "Jane")
// Output: Failed to create instance: ...
}
如果希望通过类型实参指定目标类,可以复用刚才的构造函数:
kotlin
inline fun <reified T : Any> createInstanceReified(vararg args: Any?): T? =
createInstance(T::class, *args)
这里仅省去了显式传入类引用,参数数量、类型以及构造函数的可访问性仍需在运行时检查。
依赖说明
本文的反射示例需要 kotlin-reflect,前面的 JSON 示例需要 Gson。以下仅为依赖写法示例,kotlin-reflect 应与项目使用的 Kotlin 版本保持一致:
kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
implementation("com.google.code.gson:gson:2.10.1")
总结
这些例子里,泛型主要用来表达类型之间的关系:存入与取出的类型保持一致、输入与输出的类型随转换变化、相同结构承载不同的数据。读代码时,先看类型参数关联了哪些位置,再看它需要什么约束,会比单独记住每种写法更容易理解。
泛型可以帮助编译器检查这些关系,但类型擦除、反射和外部数据仍有各自的边界,不能仅凭一个类型参数就认定运行时操作一定安全。
如果某些概念还不太清楚,可以回头看看《写了这么久 Kotlin,你真的理解 in、out 和 * 吗?》,再熟悉一下泛型基础。