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