Kotlin泛型基础
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
被推断为 Int
,Box("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
}
in
和 out
关键字与类型参数的变型有关,我们稍后会详细介绍。现在,假设我们有一个简单的实现:
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
)和不变。
-
协变(
out
): 协变使用out
关键字表示。当一个类型参数使用out
修饰时,它只能作为输出(返回值),不能作为输入(参数)。例如,在前面的Mapper
接口中:interface Mapper<in T, out R> { fun map(input: T): R }
R
是协变的,因为它只作为map
函数的返回值。这意味着如果我们有Mapper<String, Number>
,那么它也可以被当作Mapper<String, Int>
使用,因为Int
是Number
的子类型。 -
逆变(
in
): 逆变使用in
关键字表示。当一个类型参数使用in
修饰时,它只能作为输入(参数),不能作为输出(返回值)。在Mapper
接口中,T
是逆变的,因为它只作为map
函数的参数。这意味着如果我们有Mapper<Number, String>
,那么它也可以被当作Mapper<Int, String>
使用,因为Int
是Number
的子类型。 -
不变: 默认情况下,类型参数是不变的。例如,
Box<Int>
和Box<Number>
之间没有子类型关系,即使Int
是Number
的子类型。
星投影
星投影是 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 中,集合类广泛使用了泛型。例如,List
、Set
和 Map
等:
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
接口,并指定了具体的类型参数 String
和 Int
。
泛型的嵌套使用
泛型可以嵌套使用。例如,我们可以有一个 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
有两个类型参数 T
和 U
,其中 U
用于内部的 Box
类型。
通过以上对 Kotlin 泛型基础的详细介绍,我们了解了泛型在 Kotlin 中的广泛应用,包括泛型类、接口、函数,以及类型变型、约束等重要概念。泛型使得我们能够编写更通用、更安全的代码,在实际的 Kotlin 开发中是非常重要的一部分。在后续的学习和实践中,熟练掌握泛型的使用可以大大提高代码的质量和复用性。例如,在大型项目中,基于泛型构建通用的数据结构和算法库,可以减少重复代码,提高开发效率。同时,合理使用泛型的变型和约束,能够确保代码在不同类型之间的交互更加安全和可靠。希望通过本文的学习,读者对 Kotlin 泛型有了更深入的理解,并能够在实际编程中灵活运用。