前言
学习知识需要提前设立目标,带着问题学习才能有的放矢。无论是java的泛型还是kotlin语言的泛型均是写框架,写通用工具类神器。如果不熟悉泛型语法,开发过程中将会遇到很多奇奇怪怪的问题。当然语言的高级特性肯定也理解不了。
本blog基于 《kotlin实战》 第九章泛型的理解而来
java 1.5 引入泛型,目的是运行时可以动态替换泛型参数的类型,泛型参数类型T在泛型内可以出现在{对象,属性,[方法形参,返回值]}位置, 泛型实参必须是引用类型 {class,interface,map,int[],set,list}
1.泛型函数和类的声明
kotlin 引入新概念:实化类型参数、声明点变型、使用点变形
实化类型参数:泛型函数的类型参数修用 refixed 饰符 如 :< refixed T> ,并且设置泛型函数为inline 内联函数,那么在运行时可以获取到泛型参数的泛型实参的具体类型。(普通的类和函数不行,非inline函数实参运行时类型信息会被擦除)
声明点变形: 可以说明一个带类型参数的泛型类型是否是另一个泛型类型的子类型,它们基础类型一致,类型参数不同
//声明的地方变型
interface Compare< A>{ }
interface Compare< B>{ }
使用点变形:可以达到和java通配符( ?)一样的效果
interface Compare{
//使用点变型
fun <T> compare(o1:T,o2:T){ }
}
1.1 泛型类型参数(泛型类)
class A<T>{
}
class B :A<String>()
如上:两个类A、B,类A后紧跟着尖括号中的T,称为类A的类型参数或者类型形参,而类B衍生自类A,并且对类A的泛型参数进行了实化,类B后面紧跟着类A的尖括号中的String类型,成为类型实参,也可以说成类B中对类A的类型参数使用String类型进行了实化。可以类比为参数的初始化赋值。
当然类型参数一个类不止可以声明一个类型参数,也可以声明N个,比方说kotlin的Functions.kt声明了22个之多:
//Functions.kt类
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
tips: java 语言允许使用没有类型参数的泛型类型(原生态类型)是因为java 1.5 才引入泛型,需要保证与老版兼容,而kotlin不支持原生类型,类型参数必须定义,因为kotlin一开始就支持泛型类型。
//①
ArrayList list=new ArrayList();
//②
val list: ArrayList<*> = ArrayList<Any?>()
1、2分别代表java原生类型,不带类型参数,②kotlin语言的等价写法必须定义类型参数
1.2 泛型函数和属性
fun <T> List<T>.slice(indices:IntRange):List<T>
class A<T>(val a: T) {
}
说明: 类型形参声明,接收者和返回值使用了类型形参T。泛型类型必须定义,如果系统可以推导出来就不需要手动指定泛型类型了。比如
val letter =('a' ..'z').toList()
//下面两种写法一种是未指定泛型实参类型,一种是指定了泛型实参,这是等价的,
//因为第二种系统可以通过letters集合中存储的值推导出来T的值是Char.
println(letters.slice<Char>(0..2))
println(letters.slice(0..2))
1.3 声明泛型类
kotlin通过在类名称后加上一对尖括号,并把类型参数放在尖括号中来声明泛型类或者
泛型接口的
class A<T>{}
interface List<T>{
operator fun get(index:Int):T
}
在接口内部T可以当做普通类型使用,如果你的类继承了泛型类,那么就需要用泛型实参来对泛型形参进行实化。类型实参可以是具体的类型或者另一个类型形参
class ArrayList<T> :List<T>{
override fun get(index:Int):T = ...
}
注意这里的ArrayList的类型形参T,和List 中的T不是一个T,名字可以都叫T,或者不叫T。叫其他的abc,Ac 都可以
class ArrayList<B> :List<T>{
override fun get(index:Int):T = ...
}
1.4 类型参数约束
为什么需要对泛型类型参数做约束,可以限制做为泛型类和泛型函数的类型实参的类型,限制List集合中只能添加衍生自某种类型的子类型或自身称为上界约束,限制集合中只能添加特定类型的超类称为下界约束,这里可以结合java泛型的声明:<? super T>和 <? extends T>
java写法:
<T extends Number> T sum(List<T> list)
<T super Number> T sum(List<T> list)
kotlin扩展函数写法:
fun<T:Number> List<T>.sum():T
极少数情况下需要在一个类型参数上指定多个约束,如果需要可以使用where关键字,
fun<T> ensureTrailingPeriod(seq:T):where T:CharSequence,T:Appendable{
}
这种情况作为类型的实参的参数类型必须同事实现CharSequence和Appendable两个接口。
1.5 让类型形参非空
默认情况下泛型类型T的类型是Any?,也就是泛型类型可以使用空值和非空值进行赋值。那么如何限制类型形参必须为非空值能,只需要显式对泛型参数做一个非空约束就可以实现。
//默认类型是T:Any?
class Processor<T:Any>{
fun test(value:T){
value.hashCode()
}
}
2.实例化类型参数和类型擦除
jvm上泛型一般是通过类型擦除实现的,泛型类型实例的类型实参在 运行时是不保留的。
2.1 类型检查和转换
kotlin的泛型在运行时也被擦除了,意味着泛型类实例不会携带用于创建它的类型实参的信息。
val list=listof(1,2,3)
val strList=listof("a","b","c")
在运行时list、和strList你不能知道他们是否声明成字符串,整数列表或者其他对象列表。因为运行时不附带任何类型实参信息。这么做的好处就是节省内存,内存中保存的类型信息更少。
那么使用 is List也是不可以的。但是可以星号投影来判断类型是否是一个List列表而不是Set列表。<*>类似Java中的<?> 表示拥有未知类型。
fun printSum(c:Collection<*>){
val intList=c as? List<Int>?:throw IllegalArgumentException("List is expected")
println(intList.sum())
}
fun main(args: Array<String>) {
printSum(listOf(1,2,3))
printSum(listOf("1","2","3"))
}
这个函数表示对集合求元素数,对第一种情况正常输出 6,而第二种情况对String类型的列表求和,就会提示类型转换异常
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Number
如何避免这种异常情况呢?
kotlin有特殊语法结构可以允许你在函数体中使用具体的类型实参,但只有inline函数可以
2.2 声明实化类型参数的函数
inline fun <reified T> isA(value:Any)=value is T
如上isA函数被声明成了inline函数并且地泛型参数进行reified修饰。(reified 实化修饰符),reified声明了类型参数不会在运行时被擦除。
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
return filterIsInstanceTo(ArrayList<R>())
}
fun main(args: Array<String>) {
println(listOf(1,2,"abc").filterIsInstance<String>())
// printSum(listOf("1","2","3"))
}
系统级别的实化api,遍历列表中元素,判断元素是否是指定类型的对象。
为什么实化只对内联函数有效?
因为内联函数的字节码码会被编译到调用处,每次你调用实化类型参数的函数时,编译器知道这次特定调用中用做类型实参的确切类型。调用的时候filterIsInstance< String>(),传递的类型实参。因为生成的字节码引用了具体的类,而不是类型参数,它不会的被运行时发生的类型参数擦除影响。
tips:
带reified类型参数的inline函数不能在java代码中调用,普通内联可以像常规函数那样在java中调用,但是失去了内联的特性。带类型实化参数的函数需要特殊处理,把类型实参的值替换到字节码中。必须永远是内联的,所以不能用java普通的方式调用。
2.3 实化类型代替类引用及使用的限制
实化reified使用场景。
inline fun <reified T:Activity> Context.startActivity(){
val intent=Intent(this,T::class.java)
startActivity(intent)
}
实化类型T,在函数中可以当做具体类型使用
可以用在:
- 类型检查和类型转换中(is,as?)
- 使用kotlin的反射api
- 获取Class(::class.java)
- 作为调用其他函数的类型实参
不能做:
- 创建指定类型参数的类的实例
- 调用类型参数的伴生对象
- 调用实化类型的参数函数的时候使用非实化类型形参作类型实参
- 把类,属性或者非内联函数的类型参数标记成reified
3.变型:声明点变形和使用点变形
变型:用来描述具有相同的基础类型和不同的类型实参(泛型)的类型之间是如何关联的。例如List<String> 和List<Any>,正确理解变形有助于创建既不会以不方便的方式限制用户,也不会破坏用户所期望的类型安全
fun printContents(list:List<Any>){
println(list.joinToString())
}
fun main{
printContents(listof("a","b"))
}
>>>> a,b
函数把每个元素当成Any对待,因为字符串是Any,这是完全安全的。那么有一种情况需要考虑,如果形参是List函数接口类型还会安全吗?
fun printContents(list:MutiableList<Any>){
list.add(100)
}
这种就是不安全的类型,因为在函数中对List的元素进行了编辑.如果按照上面的方式进行调用就会报类型转换异常。
3.1 类、类型、子类型
变量的类型规定类该变量可能的值,有时候可以把类型和类看成同样的概念使用。但是他们并不会完全一样的。
非泛型类:可以直接使用类名作为类型来使用。
var x:String
var x:String?
一个kotlin类可以用于构造至少两种类型【可空类型】和【非空类型】
泛型类的情况就变得更复杂了,要得到一个合法的类型的需要一个的类型实参对类型形参进行替换。List不是一个类型,但下面列举出来的所有替代品都是合法的类型。List、List<List>,每一个泛型类都可能生成潜在的无线数量的类型。
子类型、超类型是一对反义词。放在具体的上下文语境中。任何时候如果需要的是类型A的值,都能够使用类型B的值进行替换。类型B就是类型A的子类型。反之类型A是类型B的超类型。编译器每一次给变量赋值或者给函数传递实参都要做这项检查(实参的类型是否是形参类型的子类型。)
子类型和子类,本质上意味着是一样的事物,但是kotlin同一个类的可空类型和非空类型,并不遵循这个。A是A?的子类型。反过来不成立。
不变类型:一个泛型类 MutableList 如果对于任意两种类型A和B,MutableList 既不是MutableList 的子类型也不是它的超类型,MutableList就被称为在该类型参数上是不变型的。在kotlin语言环境下来说,java中的类都是不变型。(可读类,Mutiable修饰可读写类)
3.2 协变,逆变
协变:保留子类型关系化,修饰符:<out T>只能用在返回值位置
逆变:反转子类型化关系,修饰符<in T>,只能用在入参位置。
interface TransFormer<out T>{
fun transform(t:T):T
}
这里入参声明 t:T 是in 位置,返回值T 是out位置,类型参数T上关键字out有两层含义。
- 子类型化会被保留(Producer<Cat>是Producer<Animal>的子类型)
- T只能用在out位置
例如:Kotlin类型List的声明导致List是可读列表,不能对List进行add元素。
//系统声明 List<out T>保留子类型化会被保留
public interface List<out E> : Collection<E> {
}
//
interface Comparator<in T>{
fun compare(e1:T,e2:T):Int{}
}
协变 | 逆变 | 不变型 |
Producer< out T> | Consumer< in T> | MutableList< T> |
类的子类型化关系保留了:Producer< Cat>是Producer< Animal>的子类型 | 子类型化反转了,Consumer< Animal>是Consumer< Cat>的子类型 | 没有子类型化 |
T只能在out位置 | T只能在ini位置 | T可以在in或者out位置或者其他任意位置 |
Tips:
kotlin 的表示法(P)->R 是表示Function<P,R>另一种可读性的形式,可以写成
<in P,out R>,意味着这个函数类型的第一个类型参数,子类型化反转了,第二个类型参数子类型化保留了。
3.3 使用点变型:在类型出现的地方指定变型。
声明点变型:在类声明的时候指定参数类型使用变形修饰符是很方便的。修饰符修饰过之后,会应用到所有类被使用的地方。
使用点变形:每次在使用带类型参数的类型时,可以指定类型餐宿是否可以用它的子类型或者超类型替换.比如java的通配符语法<? extends>,<?Super>;
kotlin也支持使用点变形,允许在类型参数出现的具体位置指定变形。(即使类型声明时不能被声明成协变或逆变)
MutableList既不是协变也不是逆变,因为它同时生产和消费指定为它们类型参数的类型的值,但是对于这个类型的变量来说常用的场景是,在某个特定函数中当成一种角色使用情况挺常见。
fun <T> copyData(source:Mutiable<T>,destination:MutableList<T>){
for(src in source){
destination.add(src)
}
}
要让函数支持不同类型的列表,可以引入第二个泛型参数
fun <T:R,R> copyData(source:Mutiable<T>,destination:MutableList<R>){
for(src in source){
destination.add(src)
}
}
可以使用变形修饰符更优雅的实现相同的功能
fun <T> copyData(source:Mutiable<out T>,destination:MutableList<int T>){
for(src in source){
destination.add(src)
}
}
tips:
kotlin 使用点变型直接对应java的限界通配符,kotlin中的MutableList<outT>对应java中的MutableList<? extends T>是一个意思,in 投影的MutableList<in T> 对应java的MutableList<? super T>,使用点变型有助于放宽可接收类型的范围。
3.4 型号投影:使用*代替类型参数
星号投影:表明不知道关于泛型实参的任何信息
MutiableList<> 与MutiableList<Any?>不一样,(MutiableList 在T上是不变类型的),MutiableList<Any?>可以包含任意类型的元素,MutiableList<>包含特定类型的元素,具体那种元素不需要关心。
MutiableList<*>中包含特殊类型的列表,具体类型不清楚,就导致一个问题,这种类型的集合只能读取不能写入任何元素(不知道可以存储那种类型,存入那种类型都会报错)
总结:
kotlin 的泛型和java相当接近,同样的方式来声明泛型类和泛型函数,kotlin泛型,类型实参也会在运行时被擦除,所以不能使用is运算符,可以将函数声明为inline ,类型参数标记为reified 实化后即可在函数运行时获取到泛型参数的类型实参来使用is判断。变型是描述拥有相同基类不同类型参数的泛型类之间子类型化关系的方式的。它说明其中一个泛型参数是另一个泛型参数的子类型,反之是超类型。可以声明一个类在某个类型参数上是协变的,如果该参数只用在out位置,逆变正好相反,只用在in位置,kotlin中List声明成协变,那么List< String> 是List< Any>的子类型。MutiableList函数类型,可以声明在第一个参数上逆变,第二个参数上协变。使(Animal)->Int 成为(Cat)->Number的子类型。kotlin中既可以为整个泛型类指定变型(声明点变形),也可以为泛型类型特定的使用指定变型(使用点变型);当确切的类型实参是未知或者不重要的时候,可以使用投影语法
泛型类型与java类型之间的映射关系java泛型 类型T 和 通配符?关系