别再只会写 <T>:Kotlin 泛型的实战用法

上一篇介绍了 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 和 * 吗?》,再熟悉一下泛型基础。

相关推荐
FlightYe2 小时前
音视频修炼之容器与协议(一):MP4和MOV的box剖析
android·linux·c++·ffmpeg·音视频
Vitaliy Kostrov(柯维远)3 小时前
客户端如何检查直播流:从流内标记到停止播放
android·运维·服务器·驱动开发·网络安全·系统架构·cdn
恋猫de小郭4 小时前
Dart 3.15 新特性:this 终于支持类型提升了
android·前端·flutter
ii_best4 小时前
ios手机开发安卓脚本开发软件[按键精灵教程]从「找最小值」到「Boss 最近刷新」再到「屏幕范围极值」
android·ios·智能手机·自动化·按键精灵
开开心心就好5 小时前
安卓PDF处理工具,免费无广告零弹窗
android·服务器·开发语言·算法·pdf·ocr
墨狂之逸才5 小时前
扫码枪响了,应用却没收到:一次 Android USB 线材问题排查
android
alexhilton16 小时前
Room Fts4:全文搜索替代 SQL LIKE
android·kotlin·android jetpack
hai_android1 天前
把 Android App 打包成系统应用(System App)
android·kotlin·android jetpack
JMchen1 天前
【Jetpack 内核实战 01】开篇:Jetpack 组件全家桶核心逻辑与专栏完整规划
android·kotlin·android jetpack