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