Kotlin泛型-你可能需要知道这些
本博文主要讲解一些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的泛型很多都是相同的,只是表现形式不太相同,我们对比学习来加深理解和记忆。
祝各位看官工作愉快。
原文地址:https://www.jianshu.com/p/9f5e355a1e65
相关推荐
-
Java开发神器Lombok的安装与使用 Java基础
2019-8-21
-
关于Class对象、类加载机制、虚拟机运行时内存布局的全面解析和推测 Java基础
2019-3-24
-
aop初探 Java基础
2019-3-4
-
Java多线程之深入解析ThreadLocal和ThreadLocalMap Java基础
2020-6-13
-
2017全年Java书单整理 转 Java基础
2020-7-3
-
分布式服务框架之远程通讯技术及原理分析 Java基础
2018-8-19
-
不使用第三方框架编写的多线程断线续传功能 Java基础
2020-6-16
-
[springboot 开发单体web shop] 8. 商品详情和评价展示 Java基础
2020-6-13
-
并发编程之显式条件 Java基础
2019-9-15
-
JVM中有哪些内存区域,分别是用来干什么的 Java基础
2019-8-18