一、概念
- 用于限定实例持有的数据类型,在类/接口内部使用统一的数据类型,也保证了容器类写入的数据类型单一化,避免运行时的类型转换异常,传入其他数据类型在编译时期就会报错提示。
T 是一个占位符,实例化或传参的时候会被替换成具体的类型。<T>针对的是类,可以赋值为非泛型类或泛型类,Demo<T>针对的是类型,只能传入基础类型是Demo的泛型类。
Create<Animal>,Create叫做基础类型(原始类型),Animal叫做泛型类型(实参类型、参数类型)。方法的泛型可以单独定义,不一定要用包裹它的类/接口的。
//当类定义了泛型
class One<T> {
fun method1(param : String){} //普通方法,没必要出现
fun method2(param : T){} //使用类定义的泛型类型
fun <R> method3(param : R){} //使用自己单独定义的泛型类型
fun method4(param : Demo<T>){} //形参为泛型类的时候,使用类定义的泛型类型
fun <V> method5(param : Demo<V>){} //形参为泛型类的时候,使用自己定义的泛型类型
}
//当类未定义泛型
class Two{
fun method1(param : String){} //普通方法
fun method2(param : T){} //不存在这种东西,使用泛型见下一行
fun <R> method3(param : R){} //使用自己单独定义的泛型类型
fun method4(param : Demo<T>){} //不存在这种东西,使用泛型见下一行
fun <V> method5(param : Demo<V>){} //形参为泛型类的时候,使用自己定义的泛型类型
}
//使用
val list1 = listOf(1, 2, 3) //编译器能自动推算出Int类型
val list2: List<Long> = listOf(1, 2, 3)
val list3 = listOf<Long>(1, 2, 3)
二、泛型擦除
和Java一样(Java 5 引入泛型为了兼容老版本,在生成字节码的时候将泛型信息擦除掉了),Kotlin的泛型在运行时也被擦除了,因此无法检查传入对象的泛型参数是什么(但是可以检查对象的类型)。这样的好处是节省内存。看似不安全,但在编译的时候需要指定了泛型类型来确保只能传入对应类型的对象。
2.1 类型检查 is、!is
在kotlin中,如果你检查过一个变量是某种类型,后面就不需要再转换它,可以把它当做你检查过的类型来使用。
fun <T> demo(param: T) {
//无法检查对象的泛型类型
if (param is List<String>) {}
//可以检查对象是否是List类型
if (param is List<*>) {}
//若果在编译时期已经知道类型信息,is检查是被允许的
val list = listOf<Int>(1,2,3)
if(list is List<Int>){}
}
2.2 类型转换 as、as?
用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,使用as?运算符就会返回值null。在Kotlin中,父类是禁止转换为子类型的。
val a: Int = 3
val b = a as Number //方式一
val c: Number = a //方式二
2.3 泛型实化 reified
- inline 修饰的函数,会在调用时直接将方法体中的代码复制过去,这样我们的代码就知道了调用处指定的泛型类型是什么,就没有了类型擦出的影响。使用 reified 修饰表示 T 需要进行泛型实化,这样 T 就能被当做一个类型使用了。
- 泛型实化允许我们在泛型函数中获得泛型的具体类型,使得【a is T】以及【T::class.java】的语法成为可能。
//T无法用来检查类型
fun <T> demo2(param:Any){
if(param is T){}
}
//使用 reified 关键字便可以是否是T类型,但只能用在 inline 函数上
inline fun <reified T> demo3(param:Any){
if(param is T)
}
inline fun <reified T> toActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
Activity {
//原来
val intent = Intent(this,TestActivity::class.java)
intent.putExtra("age", 18)
startActivity(intent)
//现在
toActivity<TestAcvivity>(this) {
putExtra("age", 18)
}
}
三、泛型约束
只有上限定:可以传入 T 及其子类类型。
多个上限定使用 where 关键字连接。
未指定默认为 Any?,若想限制形参不为 null 需要手动指定为 <T : Any>。
//类、接口
class Demo<T : Animal>{}
class Demo<T> where T : Animal, T : Eat {}
class Demo<T> : Person() where T : Animal,T : Eat {}
//方法
fun <T:Animal> method(param:T){}
fun <T> method(param: T) :Unit where T : Animal, T : Eat {}
//对比Java
class Demo<T extends Person & IEat & IRun>{}
public <T extends Person> void method(Demo<T> param){}
四、型变(通配符 约束)
- 不变型:List<T>是个泛型类,那么 List<A> 和 List<B> 正常情况下是没有关系的两个不同的数据类型,由于不存在子父类型关系,声明为什么类型就只能赋值什么类型。
- 型变:
- 泛型类:若 A 和 B 存在子父类关系,使用类型参数约束或者 in/out 约束,List<A> 和 List<B> 便有了子父类型关系,可以相互传参赋值,但为了安全会有读写限制。
- 非泛型类:一个类(Int类)对应两种子类型:非空类型(Int)、可空类型(Int?)。非空类型又是可空类型的子类型。
- 使用场景:producer-out,consumer-in。数据使用的时候只存在读取场景使用 out,只存在写入的场景使用 in。
- 声明点型变:在类/接口上使用,简化了成员变量和成员函数使用 T 每次都需要写 in/out 声明的烦恼。
- 使用点型变:在具体使用的地方(函数、变量)上声明。当某个形参有明确的只读/只写操作就可以使用 in/out 修饰。
4.1 协变 covariance:关键字 out
<out T>:相当于Java的上限通配符 <? extends T>。
- 用在类/接口:意味着该类型数据全部使用场景只用来输出(该类的功能设计是用来生产的)。
- 用在成员变量:只能作为 val 的数据类型。
- 用在成员函数:使用 T 只能放在 out 位置(返回值类型)。
- 用在变量:例如 List<out E>是个协变类
- 只能赋值为子类型实例:List<Number> 父类型变量引用赋值为List<Int> 子类型实例对象,从多态的角度来说具体子类(Int、Float)可以向上转型为更宽泛的父类(Number),因此可以大胆的从 List<Number> 中读取Number 数据。而如果赋值为更宽泛的父类型实例对象List<Any>,从 List<Number>子类型对象引用读取List<Any> 父类型实例对象中的 String 数据肯定是无法做到的。
- 只读:可以写入null。往 List<Number> 父类型变量引用中写入 Number/Double 数据对于List<Int> 子类型实例对象显然是无法接收的。
- 用在函数:不管实际传参是什么子类型对象,都可以读取 T数据。无法调用形参涉及写入的方法。
//类、接口
interface Demo<out T>{
//T用在成员变量上:只能是val
val param: T
val param: Create<T>
//T用在成员函数上:只能放在out位置
fun method() : T {}
fun method() : Create<T> {}
}
//函数
fun method(param: out T) {}
fun method(param: Create<out T>) {}
//用在变量上,List<out E>本身就是协变的
val num: List<Int> = listOf()
val list: List<Number> = num //赋值,List<Int>是List<Number>子类型
val i: Int = 3
val d: Double = 3.14
list.add(i) //报错,连Int也无法写入
list.add(d) //报错,Double是Number子类无法写入
//用在函数上
fun method(param: List<Number>){ //传参,可以传List<Int>子类型实参
param.add(3.14) //报错:List<Number>形参可以写入Float数据,但是传入的List<Int>实参无法写入Float
}
4.2 逆变 contravariance:关键字 in
<in T>:相当于Java的下限通配符 <? super T>。
- 用在类/接口:意味着该类型数据全部使用场景只用来输入(该类的功能设计是用来消费的)。
- 用在成员变量:只能作为 var 的数据类型。
- 用在成员函数:使用 T 只能放在 in 位置(参数列表)。
- 用在变量:例如 Demo<in T> 是个逆变类
- 只能赋值为父类型实例:Demo<Human> 子类型变量引用赋值为 Demo<Creature> 父类型实例对象,从多态的角度来说更宽泛的父类(Creature)是可以接收具体子类(Animal、Human、Coder),因此可以大胆往Demo<Human> 中写入 Human/Coder 数据。而如果赋值为更具体的子类型实例对象 Demo<Coder>,往Demo<Human>父类型变量引用写入Human 数据对于 Demo<Coder> 子类型实例对象来说肯定是无法接收的。
- 只写:可以读取Any。从 Demo<Creature> 父类型实例对象中读取 Animal 数据对于 Demo<Human> 子类型变量引用肯定是无法做到的。
- 用在函数:不管实际传参是什么父类型对象,都可以写入 T 数据。无法调用形参涉及读取的方法。
//类、接口
interface Demo<in T>{
//T用在成员变量上:只能是var
var param: T
var param: Create<T>
//T用在成员函数上:只能放在in位置
fun method(param: T)
fun method(param: Create<T>)
}
//方法
fun method(param: in T) {}
fun method(param: Create<in T>) {}
class Demo<in T> {}
open class Creature {}
open class Human : Creature() {}
class Animal : Creature() {}
class Coder : Human() {}
val creature: Demo<Creature> = Demo()
val human: Demo<Human> = creature //赋值,Demo<Creature>是Demo<Human>父类型
fun method(param: Demo<Human>) { //传参,可以传Demo<Creature>父类型实参
param.get() //报错:传入的Demo<Creature>实参可以包含Animal数据,但形参Demo<Human>是无法读出来Animal
}
4.3 星号投射 star-projection :关键字 *
< * >:相当于Java的通配符< ? >,可看作是<out Any?>可 赋值/传参 任意类型,但是无法写入数据。
如果类/接口上已经使用了in/out,那么变量声明里使用<*>不会更改原限制。
单个类型参数 | Demo<*>等价于 |
Demo<T:Animal> | 读取:Demo<out Animal> 写入:Demo<in Nothing> |
Demo<out T:Animal> | 读取:Demo<out Animal> |
Demo<out T> | 只读:Demo<out Any?> |
Demo<in T> | 只读:Demo<in Nothing> |
Demo<T> | 读取:Demo<out Any?> 写入:Demo<in Nothing> |
多个类型参数 | 等价于 |
Demo< * , String> | Demo<in Nothing, String> |
Demo<Int, * > | Demo<Int, out Any?> |
Demo< * , * > | Demo<in Nothing, out Any?> |
class Demo<out T:Number>{}
val demo:Demo<*> = Demo() //等价于 val demo:Demo<out Number> = Demo()
五、面试
①型变中的 T 不一定只能出现在 in/out 位置:
- T能出现在构造函数中。型变的初衷是接受更宽泛的赋值范围,由于安全性做了只读只写限制,而构造函数在实例化后不会再被调用,因此 T 在这里很安全。
- List<out E> 是个协变类,但是里面的 contaiins() 方法参数列表中出现了 T,只要能保证函数内部不会对 T 进行写入操作就行,需要使用 @UnsafeVariance 注释,相当于告诉编译器安全性自己能把控。(实际开发中不能一味的定义成协变/逆变,会丧失对于 T 使用的灵活性,也很难做到使用 T 都是出现在对应的位置,只要保证不出现相违背的读写操作就行)。
//List<out E>源码
override fun contains(element: @UnsafeVariance E): Boolean
②以下针对的是 类/接口/函数 在声明时候的写法:
泛型声明 | 泛型约束声明 | ||
类/接口 | class Demo<T> 声明只能针对类,无法针对类型。 实例化可以赋值为泛型类或非泛型类 | class Demo<T : Animal> 指定具体上界类型Animal的同时还能使用T,T在类/接口中使用没有限制。只能上限定,没有下限定。 | |
声明点型变:类/接口中使用T不用重复声明变型。成员函数使用T只能出现在out位置,成员变量使用T只能出现在var位置。 | class Demo<out T> class Demo<out Animal> 指定具体上届类型和使用T只能二选一。 | ||
class Demo<out T : Animal> 指定具体上界类型Animal的同时还能使用T。 | |||
函数 | fun <T> method(param : T) 声明针对的是:类 可以传参泛型类或非泛型类 | fun <T : Animal> method(param : T) | |
fun <T> method(param : Demo<T>) 声明针对的是:类型 只能传参泛型类 | fun <T : Animal> method(param : Demo<T>) 只能上限定,没有下限定。 | ||
使用点型变 | fun method (param : Demo<out T>) 形参只读取(生产)数据,因此可使用out修饰。相当于Java中的通配符形式Demo<? extends T>,一样只能用来修饰类型。 |
③能直接实例化泛型吗?
T t = new T(); //Java
val t: T = T() //Kotlin
不能,编译出错。如果传入的泛型参数是 <String>,被擦除后会被编译成 <Object>类型,显然是不对的。由于无法在编译阶段确定泛型参数的类型,为了防止类型异常,因此无法直接对泛型直接实例化。
④泛型擦除后,一定会被编译成Object吗?
class Demo<T extends String> //Java
class Demo<T: String> //Kotlin
如果没有指定上界,泛型擦除后会被编译成 Object类型,指定了上界,会被编译成上界 String类型。
⑤无法获取泛型 Class 类型。
//Java
ArrayList<Integer> a1 = new ArrayList();
ArrayList<Double> a2 = new ArrayList();
System.out.println(a1.getClass() == a2.getClass()) //true
//Kotlin
val a1 = ArrayList<Int>()
val a2 = ArrayList<Double>()
println(a1.javaClass == a2.javaClass) //true
泛型被擦除后,无论 ArrayList<Integer> 还是 Array<Double> 获取到的 class 都是原始类型ArrayList.class,而不是什么 ArrayList<T>.class。
⑥泛型信息被擦除,真的不存在了吗?
编译后,字节码中是有保存泛型相关信息的,因此在运行的时候可以通过反射获取到。