MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin泛型基础

2024-10-064.9k 阅读

Kotlin 泛型基础

泛型是许多编程语言中非常重要的特性,它允许我们在定义类、接口或函数时使用类型参数。在 Kotlin 中,泛型提供了一种强大的方式来创建可复用的代码,同时确保类型安全。下面我们将深入探讨 Kotlin 泛型的基础知识。

泛型类

定义一个泛型类非常简单,只需要在类名后面的尖括号中指定类型参数。例如,我们可以定义一个简单的 Box 类来包装一个值:

class Box<T>(val value: T) {
    fun getValue(): T {
        return value
    }
}

在上述代码中,T 是类型参数。它可以代表任何类型。我们可以像下面这样使用这个 Box 类:

val box = Box(10)
val intValue = box.getValue()
println(intValue)

val stringBox = Box("Hello")
val stringValue = stringBox.getValue()
println(stringValue)

在创建 Box 实例时,我们指定了具体的类型,Box(10) 这里 T 被推断为 IntBox("Hello") 这里 T 被推断为 String

多个类型参数

一个类也可以有多个类型参数。例如,我们定义一个表示键值对的泛型类 Pair

class Pair<K, V>(val key: K, val value: V) {
    fun getKey(): K {
        return key
    }
    fun getValue(): V {
        return value
    }
}

使用这个 Pair 类:

val pair = Pair("name", "John")
val key = pair.getKey()
val value = pair.getValue()
println("$key: $value")

这里 K 通常用于表示键的类型,V 用于表示值的类型。

泛型接口

与泛型类类似,我们也可以定义泛型接口。例如,定义一个 Mapper 接口,用于将一种类型映射到另一种类型:

interface Mapper<in T, out R> {
    fun map(input: T): R
}

inout 关键字与类型参数的变型有关,我们稍后会详细介绍。现在,假设我们有一个简单的实现:

class StringToIntMapper : Mapper<String, Int> {
    override fun map(input: String): Int {
        return input.toInt()
    }
}

使用这个 Mapper

val mapper = StringToIntMapper()
val result = mapper.map("123")
println(result)

泛型函数

除了泛型类和接口,我们还可以定义泛型函数。例如,一个简单的 max 函数,用于返回两个值中的较大值:

fun <T : Comparable<T>> max(a: T, b: T): T {
    return if (a > b) a else b
}

这里 <T : Comparable<T>> 表示类型参数 T 必须实现 Comparable<T> 接口,这样我们才能在函数中使用 > 操作符进行比较。使用这个函数:

val maxInt = max(10, 20)
val maxString = max("apple", "banana")
println("Max int: $maxInt")
println("Max string: $maxString")

类型擦除

在 Java 虚拟机(JVM)上,Kotlin 的泛型实现基于类型擦除。这意味着在运行时,泛型类型信息会被擦除。例如,对于下面的代码:

fun printBox(box: Box<*>) {
    val value = box.getValue()
    // 这里无法在运行时知道 value 的具体类型
    println(value)
}

虽然在编译时我们有泛型类型信息来确保类型安全,但在运行时,所有泛型类型参数都会被擦除为它们的上界(如果指定了上界,否则为 Any)。

泛型类型的变型

Kotlin 支持三种类型的变型:协变(out)、逆变(in)和不变。

  1. 协变(out: 协变使用 out 关键字表示。当一个类型参数使用 out 修饰时,它只能作为输出(返回值),不能作为输入(参数)。例如,在前面的 Mapper 接口中:

    interface Mapper<in T, out R> {
        fun map(input: T): R
    }
    

    R 是协变的,因为它只作为 map 函数的返回值。这意味着如果我们有 Mapper<String, Number>,那么它也可以被当作 Mapper<String, Int> 使用,因为 IntNumber 的子类型。

  2. 逆变(in: 逆变使用 in 关键字表示。当一个类型参数使用 in 修饰时,它只能作为输入(参数),不能作为输出(返回值)。在 Mapper 接口中,T 是逆变的,因为它只作为 map 函数的参数。这意味着如果我们有 Mapper<Number, String>,那么它也可以被当作 Mapper<Int, String> 使用,因为 IntNumber 的子类型。

  3. 不变: 默认情况下,类型参数是不变的。例如,Box<Int>Box<Number> 之间没有子类型关系,即使 IntNumber 的子类型。

星投影

星投影是 Kotlin 中处理未知类型的一种方式。当我们不知道泛型类型参数的具体类型时,可以使用星投影。例如,对于 Box<T>Box<*> 表示 Box 可以包含任何类型。

fun printBox(box: Box<*>) {
    val value = box.getValue()
    println(value)
}

这里 Box<*> 意味着我们不知道 Box 内部具体是什么类型,但仍然可以安全地获取值并打印。如果 Box 定义为 Box<T : Number>,那么 Box<*> 表示 Box 包含 Number 或其任何子类型。

泛型约束

我们可以对泛型类型参数添加约束。例如,前面提到的 max 函数:

fun <T : Comparable<T>> max(a: T, b: T): T {
    return if (a > b) a else b
}

这里 T : Comparable<T> 表示 T 必须实现 Comparable<T> 接口。我们还可以有多个约束,例如:

interface Serializable
fun <T : Comparable<T> & Serializable> process(data: T) {
    // 处理数据,这里 data 既是 Comparable 又是 Serializable
}

在上述代码中,T 必须同时实现 Comparable<T>Serializable 接口。

内联泛型函数与具体化类型参数

Kotlin 提供了内联泛型函数和具体化类型参数的特性,这在某些场景下非常有用。例如,在运行时获取泛型类型信息。

inline fun <reified T> isInstance(obj: Any?): Boolean {
    return obj is T
}

这里 reified 关键字使我们可以在运行时获取泛型类型参数 T 的实际类型。使用这个函数:

val result = isInstance<String>("Hello")
println(result)

这种方式在需要在运行时进行类型检查的场景下非常方便,而不需要通过反射来获取类型信息,性能上也更优。

泛型委托

Kotlin 的委托机制与泛型结合可以实现非常灵活的代码结构。例如,我们可以创建一个委托给另一个泛型类型的类:

interface Repository<T> {
    fun save(data: T)
    fun load(): T?
}

class InMemoryRepository<T> : Repository<T> {
    private var data: T? = null
    override fun save(data: T) {
        this.data = data
    }
    override fun load(): T? {
        return data
    }
}

class DelegatingRepository<T>(private val delegate: Repository<T>) : Repository<T> by delegate {
    // 这里 DelegatingRepository 委托给了 delegate,它会自动实现 Repository 的方法
}

在上述代码中,DelegatingRepository 通过 by 关键字委托给了 delegate,这样它就自动实现了 Repository 接口的所有方法。

泛型与集合

在 Kotlin 中,集合类广泛使用了泛型。例如,ListSetMap 等:

val intList: List<Int> = listOf(1, 2, 3)
val stringSet: Set<String> = setOf("a", "b", "c")
val map: Map<String, Int> = mapOf("one" to 1, "two" to 2)

这些集合类通过泛型确保了类型安全。例如,intList 只能包含 Int 类型的元素。同时,集合类的操作也会根据泛型类型进行类型检查。例如:

val newList = intList.filter { it > 1 }
println(newList)

这里 filter 函数操作的是 Int 类型的元素,并且返回的新列表也只包含 Int 类型的元素。

泛型的继承与实现

当一个类继承自泛型类或实现泛型接口时,有几种方式可以处理类型参数。例如,对于前面的 Box 类:

class SpecialBox<T>(value: T) : Box<T>(value) {
    // 可以添加特殊的方法
    fun specialOperation() {
        println("Special operation on $value")
    }
}

这里 SpecialBox 继承自 Box,并使用相同的类型参数 T。对于泛型接口,例如 Mapper

class AnotherMapper : Mapper<String, Int> {
    override fun map(input: String): Int {
        return input.length
    }
}

AnotherMapper 实现了 Mapper 接口,并指定了具体的类型参数 StringInt

泛型的嵌套使用

泛型可以嵌套使用。例如,我们可以有一个 Box 类,它的成员也是一个泛型类型:

class NestedBox<T, U>(val innerBox: Box<U>, val value: T) {
    fun getInnerValue(): U {
        return innerBox.getValue()
    }
}

使用这个 NestedBox

val innerBox = Box(10)
val nestedBox = NestedBox("Outer", innerBox)
val innerValue = nestedBox.getInnerValue()
println(innerValue)

这里 NestedBox 有两个类型参数 TU,其中 U 用于内部的 Box 类型。

通过以上对 Kotlin 泛型基础的详细介绍,我们了解了泛型在 Kotlin 中的广泛应用,包括泛型类、接口、函数,以及类型变型、约束等重要概念。泛型使得我们能够编写更通用、更安全的代码,在实际的 Kotlin 开发中是非常重要的一部分。在后续的学习和实践中,熟练掌握泛型的使用可以大大提高代码的质量和复用性。例如,在大型项目中,基于泛型构建通用的数据结构和算法库,可以减少重复代码,提高开发效率。同时,合理使用泛型的变型和约束,能够确保代码在不同类型之间的交互更加安全和可靠。希望通过本文的学习,读者对 Kotlin 泛型有了更深入的理解,并能够在实际编程中灵活运用。