Kt中的泛型是一大特色!和Java不太相似,或者说是补齐了Java的坑!
简单的使用泛型,不再赘述!
在Java中,使用泛型的时候,可以使用 extend
关键字表示当前的泛型必须是某个类的子类!才能正常使用! Kt中当然也支持这样的方法! kt中叫上界!
// 这里我们指定了,T的上界是Number ! T 实际的类型,必须是 Number 或者 Number的子类!
fun <T : Number> List<T>.sum() : T
上界还有一个作用!就是指定泛型为不可空类型!如果你的上界不是一个可空类型,那么你的泛型就不能被替换成可空的类型!
类型擦除
在Java中我们都知道 泛型在运行时是被擦除的!在Kt中也是如此的,在运行时候,我们没办法区别 List<Int>
和 List<String>
但是 kt 中提供了一种有局限的解决方法!通过inline
和reified
两个关键字,来支持真泛型函数!但是也是有局限的!
// 我们 使用了内联 和真泛型
inline fun <reified T> isInstance(obj: Any?): Boolean {
if (obj == null) return false
return obj is T
}
fun main(args: Array<String>) {
val obj = "kt"
val isString = isInstance<String>(obj)
println(isString)
}
这里的泛型可以做如下的事情:
- 在类型检查和类型转换中使用 (
is
,!is
,as
,as?
) - 可以获取到 KClass , 通过
T::class
可以获取KClass,当然获取了Kt的KClass,也就能相应的获取到 Java 的Class
对象了 - 可以作为调用其他泛型函数的,类型实参
不能做以下的事情:
- 创建类型参数对应的类的实例
- 调用类型的伴生对象的方法
- 把类,属性,和非内联的设置成
reified
协变逆变
Kt中支持泛型的变型的!可以使用 out
和 in
来定义变型的方式!out
表示协变,in
表示逆变!
类和类型
什么是类!什么是类型!如果在没有泛型的类中,这很简单,一个类就是代表一种类型!但是在存在泛型的时候,我们就需要用另外的方式思考类型了!每一个含有泛型声明的类!都是一个类型构造器!当传入不同的类型的泛型的时候,会构造成不同的类型!List
接口是一个有一个泛型的类型构造器!List<Int>
,List<String>
他们是通过List
泛型构造器构造出来的类型!所以有泛型的类!可以构造出无数种类型!只要他们的泛型不同,就是不同的类型!(没有泛型的类,也能看成是含有0个泛型的类型构造器!)
通过泛型构造的类型,之间的继承关系如何呢?比如List<Any>
和List<Int>
是什么关系呢?如果我们直接定义成,两者之间没有任何的关系(Java就是这么做的),有的时候我们会发现,程序很难描述!
上图中,我们定义了 feedAll
方法 !它能喂养Animal
群,但是我们传入一个 Cat
群的时候,发现编译器是报错的!为什么?因为Herd<Animal>
和Herd<Cat>
之间没有任何的关系,没办法将一个 Herd<Cat>
类型的变量传递给Herd<Animal>
类型!这很不可思议对吧!所以说我们需要一种工具描述泛型构造器构造出来的类型之间的关系!
这就是我们要说的型变
!
型变分 协变
和 逆变
:
- 协变:如果 A 是 B 的父类,并且通过泛型构造器 L ,构造的
L<A>
是L<B>
的父类!那么就说L是协变的! - 逆变:通过泛型构造器L,
L<A>
是L<B>
的子类,就说L是逆变的!
型变
的理论只发生在有泛型的类型构造器上!
我们上面的 Herd<Cat>
应该是 Herd<Animal>
的子类才是!说我们应该讲 Herd
类设置成 协变
! class Herd<out T : Animal>
但是我们不能将所有的泛型都设置成out
或者 in
,这两者也不是乱用的!在kt中他们只能放在特定的地方!
将一个泛型设置成 out
和 in
会影响这个泛型的使用的位置!被out
修饰的泛型只能被放在out
位置!被in
修饰的只能放在in
位置!
out
位置我们指的是生产者位置!我们不能消费它!
in
位置我们指的是消费者位置,我们不能生产它!
如果违背了这样的规则,实际上类型是会出现问题的!但是在kt中编译器能帮助你正确的使用in
和out
,用错了会在编译时报错!
out 用在消费者上
举例上面的 Transformer
接口,如果此时 T
是out
的(协变的)!现有 A,B类,其中类A是类B的父类,记做A <= B
!
Transformer
是 协变的!则 Transformer<A> <= Transformer<B>
,以下代码:
val a : Transformer<A>
val b : Transformer<B> = Transformer<B>()
// 根据里氏替换原则,子类可以代替父类!即 b 赋值给 a
a = b
a.transform(/* 传入的可以是 A 或者A的子类对象 */)
// 但是 最终 transform 是通过多态调用到 b 对象的!b对象的 transform方法只能接受B或者B的子类对象!这样就冲突了!
如果 Transformer
是逆变的呢?
则 Transformer<B> <= Transformer<A>
val a : Transformer<A> = Transformer<A>()
// 里氏替换原则!
val b : Transformer<B> = a
b.transform( /* 能传入B或者B的子类 */)
// 方法也是通过多态,最终调用的是 a 对象的 `transform` 方法! 这个方法,只能接受 A 或者 A的子类!我们传入的是B或者B的子类,他们也必然是A的子类,所以没有问题!
// 但是又会出现新的问题!
val res : B = b.transform( /* someObj */)
// 我们调用 b 的 transform 方法!从接口层次看,我们能拿到一个 B对象,或者B对象的子类对象!但是这个方法是最终多态调用a的,a的transform 方法,返回的 A或者A的子类对象!此时我们就是把 A对象赋值给B类型了,违背了里氏替换原则!(父类对象不能代替子类对象!)
从上面的分析可以看得出,参数和返回值是不能用同一种型变的!参数是逆变的,返回值是协变的!所以我们分出了
in
和out
位置,位置上的泛型,不能乱用!
使用点变型
kt中我们通过在泛型类上使用 int
和 out
关键字,声明泛型的型变方式!这种声明型变的方式我们称为 声明点变型
,在Java中我们也有型变!例如我们可以使用List<? extends Object>
来接一个 List<String>
的对象!这种叫做使用点变型
!
使用点泛型
需要在使用泛型的地方,都要去使用操作符!比较的麻烦!kt这种声明式
更加的简单!
在kt中我们也支持使用点泛型
,主要的作用是,给那些,没有声明型变方式的类提供的!(Java中的泛型类,都是不型变的,kt支持使用点泛型
也是为了兼容性考虑!),你可以给没有声明型变方式的类,在调用点设置in
和out
表示型变!你不可以在使用的地方声明和类中型变方式相反的型变类型!例如kt中 List
是协变的 ,你不可以什么一个List<in T>
类型!编译器会报错的!但是你可以声明List<out T>
类型!不会报错,但是是多此一举的。
如果你在参数中,使用了使用点泛型
那么你也会限制这些对象的使用场景!
例如 MutableList
是不型变的:
val list: MutableList<in String> = MutableList<String>()
// 你不能用 String去接!只能用 Any? 去接!因为他是所有类的父类!
val i : String = alis[1]
val list2: MutableList<out String> = MutableList<String>()
// 编译出错!你不能给协变的List,添加任何的东西!因为入参是在 `in` 位置!
list2.add("hello")
我们说上面的 list
和 list2
不是一个常规的MutableList
,它们被称为投影
!受限的MutableList
使用类型投影,会导致对象的方法调用受限制!
星(*)投影
如果你不知道关于类型参数的任何信息!那么你可以使用 *
表示他们!
星投影使用在不同的地方,有不同的含义!
-
Function<*, String>
表示Function<in Nothing, String>
; -
Function<Int, *>
表示Function<Int, out Any?>
; -
Function<*, *>
表示Function<in Nothing, out Any?>
星投影,只能使用在,你对泛型具体类型不感兴趣的地方!