Kotlin入门系列:第七章 泛型

对于kotlin泛型的了解,建议需要先把java的泛型掌握,后续再看下面的文章就会轻松很多:Java泛型

1 泛型类型参数

和Java不同,Kotlin始终要求类型实参要么被显式地说明,要么能被编译器推导出来。Kotlin从一开始就有泛型,所以它不支持原生态类型(原生态类型就是例如Java中声明List可以存储各种数据类型,而不需要指明它可以存储哪种类型),类型实参必须定义。

val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()

以上两种是等价的,Kotlin能自动推导出来类型

1.1 泛型函数和属性

fun <T> List<T>.slice(indices: IntRange): List<T>

>>val letters = ('a'..'z').toList()
>>println(letters.slice<Char>(0..2)) // 指定类型为Char
>>println(letters.slice(10..13)) // 没有指定类型,Kotlin自动推导出这里是Char

输出:
[a, b, c]
[k, l, m, n]

当你在一个具体的列表上调用这个函数时,可以显式地指定类型实参。但是大部分情况下你不必这样做,因为编译器会推导出类型。

1.2 声明泛型类

和Java一样,Kotlin通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。

interface List<T> {
	operator fun get(index: Int): T
}

class StringList: List<String> {
	override fun get(index: Int): String = ...
}

class ArrayList<T>: List<T> {
	override fun get(index: Int): T = ...
}

1.3 类型参数约束

// T: Number限定了T的上界为Number,相当于Java中的T extends Number,T是Number的子类
fun <T: Number> List<T>.sum(): T

>>println(listOf(1, 2, 3).sum())

输出:
6

// 声明带类型参数约束的函数

// T类型的上界是Comparable,T必须是可比较的
fun <T: Comparable<T>> max(first: T, second: T): T {
	// first > second会根据Kotlin的运算符约定被编译成first.compareTo(second> > 0
	// 这种比较之所以可行,是因为first的类型T继承自Comparable<T>,这样你就可以比较first和另外一个类型T的元素
	return if (first > second) first else second
}

>>println(max("kotlin", "java"))
>>println(max("kotlin", 42))  // 不可比较会编译失败

输出:
kotlin
ERROR:Type parameter bound for T is not satisfied: inferred type Any is not a subtype of Comparable<Any>

// 为一个类型参数指定多个约束

// 限定了参数seq只能是CharSequence和Appendable的子类型
fun <T> ensureTrailingPeriod(seq: T) where T: CharSequence, T: Appendable {
	if (!seq.endWith('.')) { // 调用CharSequence接口定义的扩展函数
		seq.append('.') // 调用Appendable接口的方法
	}
}

>>val helloWorld = StringBuilder("Hello World")
>>ensureTrailingPeriod(helloWorld)
>>println(helloWorld)

输出:
Hello World.

// 再举一个例子
open class Fruit(val weight: Double)

interface Ground {}

class Watermelon(weight: Double) : Fruit(weight), Ground

// 限定只能切长在地上的水果
fun <T> cut(t: T) where T: Fruit, T: Ground {
	print("You can cut me.")
}

>>cut(Watermelon(3.0)) // 允许
>>cut(Apple(3.0)) // 不允许

1.3 让类型形参非空

没有指定上界的类型形参将会使用 Any? 这个默认的上界。

class Processor<T> {
	fun process(value: T) {
		value?.hashCode()  // value是可空的,尽管T并没有使用问号标记
	}
}

>>val nullableStringProcessor = Processor<String?>() // 可以设置可空类型String?
>>nullableSttingProcessor.process(null) // 可以正常编译

如果你想保证替换类型形参的始终是非空类型,可以通过指定一个约束来实现。如果你除了可空性之外没有任何限制,可以使用 Any 代替默认的 Any? 作为上界:

// 指定非空类型Any,不限定是Any,可以是任意非空类型
class Processor<T: Any> {
	fun processs(value: T) {
		value.hashCode()
	}
}

2 运行时的泛型:擦除和实化类型参数

2.1 运行时的泛型:类型检查和转换

和Java一样,Kotlin的泛型在运行时也被擦出了。这意味着泛型类实例不会携带用于创建它的类型实参的信息。例如,如果你创建了一个 List<String> 并将一堆字符串放到其中,在运行时你只能看到它时一个List,不能识别出列表本打算包含的是哪种类型的元素。

// 在运行时,你不会知道list1和list2是否声明成字符串或者整数列表,它们每个都只是List
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)

因为类型实参没有被存储下来,你不能检查它们。例如,你不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表。

>>if (value is List<String>) { ... }
ERROR:Cannot check for instance os erased type

注意擦除泛型类型信息是有好处的:应用程序使用的内存总量较小,因为要保存在内存中的类型信息更少。

Kotlin不允许使用没有指定类型实参的泛型类型。那么你可能想知道如何检查一个值是否是列表,而不是 set 或者其他对象。可以使用特殊的星号投影语法来做检查:

// 泛型类型拥有的每个类型形参都需要一个*,List<*>类比于Java中的List<?>
if (value is List<*>) { ... }

注意,在 asas? 转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出 unchecked cast(未受检转换) 的警告。你仍然可以继续使用这个值,就当它拥有必要的类型。

fun printSum(c: Colllection<*>) {
	// 这里会有警告Unchecked cast:List<*> to List<Int>
	val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
	println(intList.sum())
}

>>printSum(listOf(1, 2, 3))

输出:
6

注意,Kotlin编译器是足够智能的,在编译期它已经知道相应的类型信息时,is 检查是允许的。

fun printSum(c: Collection<Int>) {
	if (c is List<Int>) {
		println(c.sum())
	}
}

>>printSum(listOf(1, 2, 3))

输出:
6

2.2 类型擦除的矛盾

通常情况下使用泛型我们并不在意它的类型是否是类型擦除,但是在有些场景,我们却需要知道运行时泛型参数的类型,比如序列化/反序列化的时候。

那么我们有没有其他方式能够获取到类型信息呢?

2.2.1 匿名内部类获取泛型类型信息

val list1 = ArrayList<String>()
val list2 = object : ArrayList<String>() {} // 匿名内部类
println(list1.javaClass.genericSuperclass)
println(list2.javaclass.genericSuperclass)

输出:
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String>

使用匿名内部类就能获取到泛型类型信息了。那么为什么匿名内部类能获取到?

其实泛型类型擦除并不是真正的将全部的类型都擦除,还是会将类型信息放在对应class的常量池中。

既然我们知道用这种方式可以获取到泛型类型信息,那么我们自己构建一个泛型类:

import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

open class GenericsToken<T> {
	var type: Type = Any::class.java
	init {
		val superClass = this.javaClass.genericSuperclass
		type = (superClass as ParameterizedType).getActualTypeArguments()[0]
	}
}

fun main(args: Array<String>) {
	val gt = object : GenericsToken<Map<String, String>>(){}
	println(gt.type)
}

输出:
java.util.Map<java.lang.String, ? extends java.lang.String>

匿名内部类在初始化的时候就会绑定父类或父接口的相应信息,这样就能通过获取父类或父接口的泛型信息来实现。Gson也是使用了相同的设计。

val json = ...
val rType = object : TypeToken<List<String>>(){}.type
val stringList = Gson().fromJson<List<String>>(json.rType)

在kotlin除了匿名内部类的方式获取泛型类型之外,还有一种方式,就是内联函数。

2.2.2 内联函数:声明带实化类型参数的函数

如果用 inline 关键字标记一个函数,编译器会把每一次函数调用都换成函数实际的代码实现。使用内联函数还可能提升性能,如果该函数使用了lambda实参:lambda的代码也会内联,所以不会创建任何匿名类。

kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方,也就是说,参数类型也会被插入字节码中,我们就可以获取参数的类型了。

// fun <T> isA(value: Any) = value is T
// Error: Cannot check for instance of erased type: T

// 函数声明成inline并且用reified标记类型参数,就能够用该函数检查value是不是T的实例
// 在编译的会将具体的类型插入相应的字节码中,就能在运行时获取到对应参数类型了
// reified关键字可以理解为“具体化”
// 利用它我们可以在方法体内访问泛型指定的JVM对象(注意,还需要在方法前加如inline修饰)
inline fun <reified T> isA(value: Any) = value is T

>>println(isA<String>("abc"))
>>println(isA<String>(123))

输出:
true
false

我们也可以在kotlin中通过扩展函数优化Gson的使用方式:

inline fun <reified T : Any> Gson.fromJson(json: String): T {
	return Gson().fromJson(json, T::class.java)
}

val json = ...
val stringList = Gson.fromJson<List<String>>(json)

函数 filterIsInstance 这个函数接收一个集合,选择其中那些指定类的实例,然后返回这些被选中的实例。

>>val items = listOf("one", 2, "three")
>>println(items.filterIsInstance<String>()) 
输出:
[one, three]

// 函数具体实现
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
	val destination = mutableListOf<T>()
	for (element in this) {
		if (element is T) { // 类型实参在运行时是已知的
			destination.add(element)
		}
	}
	return destination
}

注意:

java并不支持主动指定一个函数是否是内联函数,所以:

  • 在kotlin中声明的普通内联函数可以在java中调用,因为它会被当作一个常规函数

  • reified 实例化的参数类型的内联函数则不能在java中调用,因为它永远是需要内联的

2.2 使用实化类型参数代替类引用

// ::class.java的语法展现了如何获取java.lang.Class对应的Kotlin类。这个Java中的Service.class是完全等同的
val serviceImpl = ServiceLoader.load(Service::class.java)

// 使用实化类型参数重写上面的例子
val serviceImpl = loadService<Service>()

inline fun <reified T> loadService() {
	return ServiceLoader.load(T::class.java)
}

// 简化startActivity
inline fun <refied T : Activity> Context.startActivity() {
	val intent = Intent(this, T::class.java)
	startActivity(intent)
}

startActivity<DetailActivity>()

2.3 实化类型参数的限制

可以按下面的方式使用实化类型参数:

  • 用在类型检查和类型转换中(is!isasas

  • 使用Kotlin反射API(::class

  • 获取相应的 java.lang.Class(::class.java)

  • 作为调用其他函数的类型实参

不能做下面这些事情:

  • 创建指定为类型参数的类的实例

  • 调用类型参数类的伴生对象的方法

  • 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参

  • 把类、属性或者非内联函数的类型参数标记成 reified

3 变型:泛型和子类型化

3.1 为什么List<String>不能赋值给List<Object>

我们知道,在java中,虽然 StringObject 的子类,但是 List<String> 是无法赋值给 List<Object> 的:

List<String> stringList = new ArrayList<String>();
List<Object> objList = stringList; // 假设可以,编译报错
objList.add(Integer(1));
String str = stringList.get(0); // 出错

在java如果允许 List<String> 赋值给 List<Object> 这种行为的话,那么它将会和数组支持泛型一样,不再保证类型安全,而java设计师明确泛型最基本的条件就是保证类型安全,所以不支持这种行为。

3.2 类、类型和子类型

在kotlin却发现了一个奇怪的现象:

val stringList: List<String> = ArrayList<String>()
val anyList: List<Any> = stringList // 编译成功

kotlin通过编译了?!不是kotlin和java的泛型原理一样的吗,怎么现在就变了?关键在于这两个List并不是同一种类型:

public interface List<E> extends Collection<E> {}
public interface List<out E> : Collection<E> {}

加了一个 out 关键字就可以做到了?

普通方式定义的泛型是不变的,简单来说就是不管类型A和类型B是什么关系,Generic<A>Generic<B> 都没有任何关系。比如刚才举例的String是Object的子类型,但是 List<String> 并不是 List<Object> 的子类型。

Kotlin中的List接口表示的是只读集合。如果A是B的子类型,那么 List<A> 就是 List<B> 的子类型。这样的类或者接口被称为协变的。

3.3 协变:在泛型保留父类型和子类型的关系

一个协变类是一个泛型类:如果A是B的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。我们说子类型化被保留了。

在Kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上 out 关键字即可:

// 类被声明成在T上协变
interface Producer<out T> {
	fun produce(): T
}

要注意的是,如果一个类或接口标记为协变的(也就是添加了 out 关键字),那么它将变成只读的,无法添加元素,只能读取内容:

val stringList: List<String> = ArrayList<String>()
stringList.add("kotlin") // 编译报错,没有add()不允许插入数据

// 使用了out关键字声明
public interface List<out E> : Collection<E> {
	...
}

你不能把任何类型都变成协变的:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。

open class Animal {
	fun feed() { ... }
}

// 类型参数没有声明成协变的,T是Animal的子类型,但在泛型中类型关系是无关的
// class Herd<T : Animal> {
//	val size: Int get() = ...
//	operator fun get(i: Int): T { ... }
//}

// 类型参数T声明在是协变的,T是Animal的子类型,在泛型中沿用继承关系
class Herd<out T : Animal> {
	...
}

fun feedAll(animals: Herd<Animal>) {
	for (i in 0 until animals.size) {
		animals[i].feed()
	}
}

class Cat : Animal() {
	fun cleanLitter() { ... }
}

fun takeCareOfCats(cats: Herd<Cat>) {
	for (i in 0 until cats.size) {
		cats[i].cleanLitter()
		// 当Herd<T : Animal>时错误:推导的类型是Herd<Cat>,但期望的确实Herd<Animal>
		// feedAll(cats) 
		feedAll(cats) // 当Herd<out T : Animal>时正确
	}
}

要保证类型安全,它只能用在所谓的 out 位置,意味着这个类只能生产类型T的值而不能消费它们。

class Herd<out T : Animal> {
	val size: Int get() = ...
	operator fun get(i: Int): T { ... } // 限定了T只能作为返回类型使用
}

总结下关键字 out 的作用:

  • 子类型会被保留(CatAnimal 的子类, Producer<Cat> 也会沿用继承关系是 Producer<Animal> 的子类型)

  • 类型只能读取,不能写入了

  • T只能用在 out 位置(传参类型T只能作为返回值)

  • 相当于java中实现协变:<? extends Object>

3.4 逆变:在泛型反转父类型和子类型的关系

支持泛型协变后的泛型类还是父子关系这可以理解,逆变反转父子类型关系这个又是什么情况?

比如 DoubleNumber 的子类型,反过来 Generic<Double> 却是 Generic<Number> 的父类型?有这种场景?

假设现在需要对一个 MutableLIst<Double> 进行排序:

val doubleComparator = Comparator<Double> {
	d1, d2 -> d1.compareTo(d2)
}
val doubleList = mutableListOf(2.0, 3.0)
doubleList.sortWith(doubleComparator)

如果我们又需要对 MutableList<Int>MutableList<Long> 等进行排序,然后定义 intComparatorlongComparator 并不是一种好的解决方法。能不能定义一个比较器让它们都可以共同使用?

val numberComparator = Comparator<Number> {
	n1, n2 -> n1.toDouble().compareTo(n2.toDouble())
}
val doubleList = mutableListOf(2.0, 3.0)
doubleList.sortWith(numberComparator)

val intList = mutableListOf(1, 2)
intList.sortWith(numberComparator)

上面的代码正常编译通过了,numberComparator 可以代替 doubleComparatorintComparator,看下 sortWith 的源码:

public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {
	if (size > 1) java.util.Collections.sort(this, comparator)
}

in 关键字和 out 一样它让泛型有了另一个特性,逆变。

简单说就是,如果类型A是类型B的子类型,那么 Generic<B> 反过来是 Generic<A> 的子类型。

out 关键字声明的泛型参数类型将不能作为方法的参数类型,但可以作为方法的返回值类型,而 in 刚好相反:

// in关键字声明,让T只能作为方法参数传入
interface WriteableList<in T> {
	fun get(index: Int): Any
	fun add(t: T): Int
}

总结下关键字 in 的作用:

  • 子类型被反转(CatAnimal 的子类, Producer<Animal>Producer<Cat> 的子类型)

  • 类型是可写的,但不能读了

  • T只能用在 in 位置(传参类型T只能作为方法参数传入)

  • 相当于java中实现逆变:<? super T>

3.5 在构造中使用协变out和逆变in

构造方法的参数既不在 in 位置,也不在 out 位置。即使类型参数声明成了 out,仍然可以在构造方法参数的声明中使用它:

class Herd<out T: Animal>(vararg animals: T) { ... }

如果你在构造方法的参数上使用了关键字 valvar,同时就会声明一个getter和一个setter。因此,对只读属性来说,类型参数用在了 out 位置,而可变属性在 out 位置和 in 位置都是用了它:

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { .... }

还需要留意的是,位置规则只覆盖了类外部可见的(publicprotectedinternal)API。私有方法的参数既不在 in 位置也不在 out 位置。变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用:

class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) { ... }

3.6 协变、逆变和不变型对比

语言协变逆变不变
kotlin实现方式:<out T>,只能作为消费者,只能读取不能添加实现方式:<in T>,只能作为生产者,读取受限实现方式:<T>,既可以添加,也可以读取
java实现方式:<? extends T>,只能作为消费者,只能读取不能添加实现方式:<? super T>,只能作为生产者,只能添加,读取受限实现方式:<T>,既可以添加,也可以读取

3.7 使用点变型:在类型出现的地方指定变型

在类声明的时候就能够指定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方。这被称为声明点变型。

在Java中,每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。这叫做使用点变型。

// source是<Int>,destination是<Any>,因为Int是Any的子类型,所以可以拷贝
fun <T: R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {
	for (item in source) {
		destination.add(item)
	}
}

// Kotlin提供了更优雅的方式
// source的类型用在out位置,其他的T类型用在in位置
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
	for (item in source) {
		destination.add(item)
	}
}

>>val ints = mutableListOf(1, 2, 3)
>>val anyItems = mutableListOf<Any>()
>>copyData(ints, anyItems)
>>println(anyItems)

输出:
[1, 2, 3]

可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型,等等。这里发生的一切被称作类型投影:我们说 source 不是一个常规的 MutableList,而是一个投影(受限)的 MutableList。只能掉哦用返回类型是泛型类型参数的那些方法,或者严格地讲,只在 out 位置使用它的方法。编译器禁止调用使用类型参数做实参(类型)的那些方法(在 in 位置使用类型参数):

>>val list: MutableList<out Number> = ...
>>list.add(42)
Error: Out-projected type 'MutableList<out Number>` prohibits the use of `fun add(element: E): Boolean`

不要为使用投影类型后不能调用某些方法而吃惊。如果需要调用那些方法,你要用的是常规类型而不是投影。这可能要求你声明第二个类型参数,它依赖的是原本要进行投影的类型。

// destination: MutableList<T>允许目标元素的类型是来源元素类型的超类型
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
	for (item in source) {
		destination.add(item)
	}
}

3.8 星号投影:使用*代替类型参数

星号投影 *,可以用它来表明你不知道关于泛型实参的任何信息。

>>val list: MutableList<Any?> = mutableListOf('a', 1, 'qwe')
>>val chars = mutableListOf('a', 'b', 'c')
// MutableList<*>和MutableList<Any>不一样
>>val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars 
>>unknownElements.add(42) // 编译器禁止调用该方法
>>println(unknownElements.first()) // 读取元素是安全的:first()返回一个类型为Any?的元素

输出:
Error:Out-projected type `MutableList<*>` prohibits the use of `fun add(element: E): Boolean` 
a

编译器会把 MutableList<*> 当成 out 投影的类型。MutableList<*> 投影成了 MutableList<out Any?>:当你没有任何元素类型信息的时候,读去 Any? 类型的元素仍然是安全的,但是向列表中写入元素是不安全的。Kotlin的 MyType<*> 对应于Java的 MyType<?>

当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型。

// 使用投影语法*,可以接收任何类型,只用于读取元素而不写入
fun printFirst(list: List<*>) {
	if (list.isNotEmpty()) {
		println(list.first())
	}
}

>>printFirst(listOf("Svetlana", "Dmitry")

泛型使用例子:输入验证接口

interface FieldValidator<in T> {
	fun validate(input: T): Boolean
}

object DefaultStringValidator : FieldValidator<String> {
	override fun validate(input: String) = input.isNotEmpty()
}

object DefaultIntValidator : FieldValidator<Int> {
	override fun validate(input: Int) = input >= 0
}

>>val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
>>validators[String::class] = DefaultStringValidator
>>validators[Int::class] = DefaultIntValidator

// 使用显式的转换获取验证器
>>val stringValidator = validators[String::class] as FieldValidator<String> // 警告:未受检的转换
>>println(stringValidator.validate(""))
false

// 错误地获取验证器
>>val stringValidator = validators[Int::class] as FieldValidator<String>
>>stringValidator.validate("") // 代码可以编译,但运行时抛出类型转换异常

// 需要实现的功能:想要把不同的类型的验证器存储在同一个地方
// 把所有对它的访问封装到了两个泛型方法中,它们负责保证只有正确的验证器被注册和返回
// 这段代码依然会发出未授检转换的警告,但这里的Validators对象控制了所有对map的访问,保证了没有任何人会错误地改变map
object Validators {
	private val validators = mutableMapOf<KClass<*>, FieldValidator<*>>)

	fun <T: Any> registerValidator(kClass: KClass<T>, fieldValidator: FieldValidator<T>) {
		validators[kClass] = fieldValidator
	}

	@Suppress("UNCHECKED_CAST")
	operator fun <T: Any> get(kClass: KClass<T>): FieldValidator<T> = 
		validators[kClass] as? FieldValidator<T>?: throw IllegalArgumentException("NO validator for ${kClass.simpleName}")
}

>>Validators.registerValidator(String::class, DefaultStringValidator)
>>Validators.registerValidator(Int::class, DefaultIntValidator)
>>println(Validators[String::class].validate("Kotlin"))
>>println(Validators[Int::class].validate(42))

输出:
true
true
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值