本博文主要讲解一些Kotlin泛型的问题,中间会对比穿插Java泛型。
1. 泛型类型参数
1.1 形式
我们使用泛型的形式无非是类、借口、方法几种,我们先看两个例子。
1.2 声明泛型类
和Java一样,我们通过在类名后面添加一对<>,并把类型参数放在<>内来声明泛型类和泛型接口。
一旦声明完成,我们就可以在类和接口内部,像使用其他类型一样使用类型参数了。
我们来看下标准的Java接口List如何使用Kotlin来声明。
Kotlin中List接口的定义。
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E
public fun indexOf(element: @UnsafeVariance E): Int
public fun lastIndexOf(element: @UnsafeVariance E): Int
public fun listIterator(): ListIterator<E>
public fun listIterator(index: Int): ListIterator<E>
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
如果你的类继承了泛型类(或者实现了泛型接口),你就得为基础类型的泛型提供一个类型实参,它可以是一个具体的类型或者另外一个类型形参。
class StringList : List<String>{
override fun get(index:Int):String =...
}
一个简单的泛型类定义:
class Pair<K, V>(key: K, value: V) {
var key: K = key
var value = value
}
1.3 泛型方法定义:
fun <T : Any> getClassName(clzObj: T): String {
return clzObj.javaClass.simpleName
}
泛型T被声明在方法名前面。
泛型T默认为可空类型,并且我们限定了T继承Any,防止clzObj为空。
1.4 和Java泛型的不同之处
(1)和Java不同,Kotlin始终要求类型实参要么被显式的声明,要么能被编译器推到出来。
val readers : MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
上面这两行代码是等价的。
(2) Kotlin不支持原生类型
Kotlin一开始就有泛型,因此它不支持原生态类型,类型实参必须定义。
2. 类型参数约束
2.1 T上界指定
如果把一个类型指定为泛型类型形参的上界约束,在泛型具体的初始化时,其对应的泛型实参类型就必须为是这个具体类型或者其子类型。
Java中的形式: T extends Number
Kotlin中的形式: T : Number
fun <T : Number> List<T>.sum():T{
//...
}
一旦指定了类型上界,你就可以在将T当做它的上界类型来使用。
如上面的例子,我们可以将T当作Number类型来使用。
2.2 为一个类型参数指定多个约束
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
//...
}
上面这个例子,指定了可以作为类型参数实参的类型必须实现了CharSequence和Appendable两个接口。
2.3 让类型形参非空
事实上,没有指定上界的类型形参将会使用Any?这个默认的上界。
看下面的这个例子:
class Processor<T> {
fun process(value: T) {
println(value?.hashCode())
}
}
在process方法中,value是可空的,尽管T没有使用任何的?标记。
如果你想指定任何时候类型形参都是非空的,那么你可以通过指定一个约束来实现。
如果你除了可控性之外没有其他限制,可以使用Any代替默认的Any?作为其上界。
class Processor<T : Any> {
fun process(value: T) {
println(value.hashCode())
}
}
注意:String?不是Any的子类型,它是Any?的子类型;String才是Any的子类型。
3. 运行时泛型
我们知道JVM上的泛型,一般是通过类型擦除来实现的,所以也被成为伪泛型,也就说类型实参在运行时是不保存的。
实际上,我们可以声明一个inline函数,使其类型实参不被擦除,但是这在Java中是不行的。
3.1 类型擦除
和Java一样,Kotlin的泛型在运行时也被擦除了,这意味着实例不会携带用于创建它的类型实参的信息。
例如你创建了一个List<String>并将一堆字符串保存其中,在运行时你只能看到一个List,不能辨别出列表本打算包含的是哪种类型的元素。
看起来类型擦除是不安全的,但在我们编码的时候,编译器是知道类型实参的类型的,确保你只能添加合适类型的元素。
类型擦除的好处就是节省内存。
3.1.1 is
因为类型擦除的原因,所以一般情况下,在is检查中不可能使用类型实参的类型。
对应的Java中,不能在一个确定泛型上执行instanceof操作。
Kotlin不允许使用没有指定类型实参的泛型类型。
那么你可能想知道如何检查一个值是否为列表,而不是Set或者其他对象,可以使用特殊的星号投影语法来做这个检查。
fun <T : Any> process(value: T) {
if (value is List<String>) {
// error
}
if (value is List<*>) {
// ok
}
var list = listOf<Int>(1, 2, 3)
if (list is List<Int>) {
// ok
}
}
Kotlin的编译器是足够聪明的,如果在编译期已经知道相应的类型信息时,is检查是被允许的。
List<*>相当于Java中的List<?>,拥有某个未知类型实参的泛型类型。
上面的例子中只是检查了value是否为List,而没有得到关于它的元素类型的任何信息。
3.1.2 as/as?
在as和as?转换中,我们仍然可以使用一般的泛型类型。
(1)如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时类型实参是未知的(因为被擦除掉了),但是后面可能会出现ClassCastException。
(2)如果该类型基础类型都不正确的话,as?就会返回一个null值。
fun main(args: Array<String>) {
var list: List<Int> = listOf(1, 2, 3)
printSum(list)// 6
var strList: List<String> = listOf("1", "2", "3")
printSum(strList)// ClassCastException
var intSet: Set<Int> = setOf(1, 2, 3)
printSum(intSet) // IllegalStateException
}
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> ?:
throw IllegalStateException("List is expected")
println(intList.sum())
}
3.2 实化类型参数函数
泛型函数的类型实参,在运行时同样会被擦除。
只有一种特殊的情况:内联函数,内联函数的类型形参能够被实化,意味着在运行时,你可以引用实际的类型实参。
// compile error
fun <T> isA(value: Any) = value is T
3.2.1 inline
如果使用inline标记函数,编译器会把每一次函数调用都替换为函数实际的代码。
lambda的代码也会被内联,不会创建任何匿名内部类。
inline函数大显身手的另一种场景:他们的类型参数可以被实化。
3.2.2 实化参数函数
inline fun <reified T> isA(value: Any) = value is T
一个实际的例子filterIsInstance:
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
return filterIsInstanceTo(ArrayList<R>())
}
public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
for (element in this) if (element is R) destination.add(element)
return destination
}
简化Android的startActivity方法:
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this,T::class.java)
startActivity(intent)
}
3.2.2 为什么实化只对inline函数有效
这是什么原理呢?为什么inline函数中可以这样写element is R,而普通的函数不行呢?
正如之前描述的,编译器把实现inline行数的字节码插入到每一次调用发生的地方。
每次你调用带实例化类型参数的inline方法时,编译器都知道这次特定调用中用作类型实参的确切类型,因此编译器可以生成引用实际类型的字节码。
实化类型参数的inline函数不能在Java中调用,普通的内联函数可以在Java中被调用。
4. 变型:泛型和子类型化
4.1 子类和子类型
Int是Number的子类。
Int类型是Int?类型的子类型,他们都对应Int类。
一个非空类型是它的非空版本的子类型,但他们都对应同一个类。
List是一个类,List<String>\List<Int>是类型。
明白子类和子类型很重要。
4.2 不变型
一个泛型类,例如MutableList--如果对于任意的两个类型A和B,Mutable<A>既不是Mutable<B>的父类型,也不是它的父类型,那么该泛型类就称为在该类型参数是不变型。
Java中所有的类都是不变型的。
4.3 协变:保留子类型化
一个协变类是一个泛型类(我们以Poducer<T>类为例),如果A是B的子类型,那么Producer<A>也是Producer<B>的子类型;我们说子类型化被保留了。
Kotlin中,要声明在某个类型参数上是可以协变的,在该类型参数的名称前加out关键字即可:
interface Producer<out T> {
fun produce(): T
}
一个不可变的例子:
open class Animal {
open fun feed() {
println("Animal is feeding.")
}
}
class Herd<T : Animal> {
val size: Int = 10
fun get(index: Int): Animal? {
return null
}
}
fun feedAllAnimal(animals: Herd<Animal>) {
for (i in 0 until animals.size) {
animals.get(i)?.feed()
}
}
class Cat : Animal() {
override fun feed() {
println("Cat is feeding.")
}
}
fun takeCareOfCats(cats: Herd<Cat>) {
feedAllAnimal(cats)//comile error
}
很遗憾,在上面的例子中我们不能把猫群当做动物群被照顾。
在没有使用任何通配符的类型参数上,泛型类在类型参数上是不变型的。
那么我们怎样才能让猫群也能被当做动物群被照顾呢,答案很简单,只需要修改Herd类如下即可:
class Herd<out T : Animal> {
val size: Int = 10
fun get(index: Int): Animal? {
return null
}
}
in & out
在类的成员声明中类型参数的使用位置可以分为in位置和out位置。
如果函数是把T当成返回类型,我们说它在out位置。
如果T用作函数参数类型,它就在in位置。
类的类型参数前使用out的关键字要求所有使用T的方法只能把T放在out位置上,而不能放在in位置。
现在考虑下,我们能否把MutableList<T>中的T声明为协变的?
答案是不能,因为MutableList既可以添加T元素,也可以获取T元素,因此T既出现在了out位置,也出现在了in位置。
在上面的分析中,我们已经看到过List接口的定义,List<T>在Kotlin中是只读的。
public interface List<out E> : Collection<E>{
//....
}
我们来看下List和MutableList的定义:
public interface MutableList<E> : List<E>, MutableCollection<E>{
override fun add(element: E): Boolean
public fun removeAt(index: Int): E
}
方法的定义验证了我们前面的分析,MutableList的类型参数E既不能声明为out E,也不能声明为in E.
构造方法中的参数既不在in位置也不在out位置,即使类型参数声明为out,我们仍然可以在构造方法参数的声明中使用它。
对于类的var属性,我们不能使用out修饰属性的类型参数。
因为属性的setter方法在in位置上使用了类型参数,而getter方法在out位置使用了类型参数。注意位置规则只覆盖了类外部可见(public\protected\internal)API,私有方法的参数既不在out位置也不在in位置。
// compile error
class Herd<out T : Animal>(var leadAnimal: T) {
}
// ok
class Herd<out T : Animal>(private var leadAnimal: T) {
}
4.4 翻转子类型化关系
逆变的概念可以看做是协变的镜像:对一个你逆变类来讲,它的子类型化关系与作用类型实参的类子类型化关系是相反的。
逆变类是一个泛型类(我们以Consumer为例),如果B是A的子类型,那么Consumer<A>是Consumer<B>的子类型。
类型A和B交换了位置,所以我们说子类型化被反转了。
逆变对应的关键字是in。
4.5 点变型:在类型出现的地方指定变型
fun <T : R, R> copyTo(source: MutableList<out T>, destination: MutableList<in T>) {
source.forEach { item -> destination.add(item) }
}
我们说source不是一个普通的MutableList,而是一个投影(受限)的MutableList,只能调用返回类型是泛型参数的那些方法。
Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一个意思。
Kotlin中的MutableList<in T>和Java中的MutableList<? super T>是一个意思。
MutableList<*>的投影为MutableList<out Any?>。
Kotlin中MyType<*>对应Java中的MyType<?>。
5. 总结
Kotlin的泛型和Java的泛型很多都是相同的,只是表现形式不太相同,我们对比学习来加深理解和记忆。
祝各位看官工作愉快。