Kotlin 进阶之路(六) 泛型
6.1 泛型的分类
- 泛型类
使用泛型标记的类,成为泛型类。泛型类使用分如下两种情况
1 泛型类被用于实例化
val list = ArrayList<String>()
val map = HashMap<String, Int>()
val set = HashSet<Long>()
2 泛型类被用于继承
当泛型类被用于继承时,需要为泛型形参提供一个具体类型或者另一个类型的形参。
class ArrayList<E> : AbstractList<E>(), List<E>, java.io.Serializable{
override val size: Int = 0
override fun get(index: Int): E {
TODO("not implemented")
}
}
ArrayList< E> 定义了一个泛型类,< E> 标识泛型形参,具体实参类型在 ArrayList 被使用时决定。AbstractList 和 List 需要的类型实参和 ArrayList 的一致。
- 泛型接口
使用泛型标记的接口,被称为泛型接口。泛型接口的使用分如下两种情况
1 泛型接口被实现时能够确定泛型接口对应的实参,直接传递实参即可。
interface List< String> : Collection< String>()
2 泛型接口被实现时不能够确定泛型接口对应的实参,则需要使用当前类或接口的泛型形参
interface List< E> : Collection< E>()
- 泛型方法
使用泛型标记的方法,被称为泛型方法。泛型方法在被调用时,只需传入具体的泛型实参即可, Kotlin 语言比较只能,在一些情况下,可以不用给具体的类型实参,程序会自动推断。
fun main(args: Array<String>) {
//String 类型为泛型实参,Kotlin 自动推断
val list = arrayListOf("a", "b", "c")
//String、Int 为泛型实参,Kotlin 自动推断
val map = hashMapOf("a" to 1, "b" to 2, "c" to 3)
//Long 为泛型实参,Kotlin 自动推断
val set = hashSetOf(1L, 2L, 3L)
}
1 高阶函数中的泛型方法
fun main(args: Array<String>) {
val letters = ('a'..'z').toList()
println(letters.slice<Char>(0..2))//调用泛型方法,显示的指定类型实参,<Char> 可省略
println(letters.slice(10..13))//编译器能自动推导出 T 是 Char
/*[a, b, c]
[k, l, m, n]*/
}
2 自定义泛型方法
自定义泛型方法的格式
修饰符 fun <泛型符号> 方法名(方法参数) : 方法返回值{
…
}
fun <T> printInfo(content : T){
when(content){
is Int -> println("传入的$content, 是一个Int类型")
is String -> println("传入的$content, 是一个String类型")
else -> println("传入的$content, 不是Int也不是String")
}
}
fun main(args: Array<String>) {
printInfo(10)
printInfo("hello world")
printInfo(true)
/*传入的10, 是一个Int类型
传入的hello world, 是一个String类型
传入的true, 不是Int也不是String*/
}
6.2 泛型约束
- 泛型约束<T : 类或接口>
1 调用泛型上界类中的方法
如果泛型约束中指定了类型参数的上界,则可以调用定义在上界类中的方法。
fun <T : Number> twice(value: T) : Double{
return value.toDouble() * 2
}
fun main(args: Array<String>) {
println("4.0 的两倍: ${twice(4.0f)}")
println("4 的两倍: ${twice(4)}")
/*4.0 的两倍: 8.0
4 的两倍: 8.0*/
}
在 twice() 方法中,参数 value 调用 toDouble() 方法是在 Number 类中定义的。由于在泛型约束 中已经指定类型参数的上界为 Number, 因此 twice() 方法中传递的参数 value 可调用定义在上界类 Number 中的方法。
如果上界约束需要多个约束,则可以通过 where 语句来完成。
fun <T> manyConstraints(value: T) where T : CharSequence, T : Appendable{
if (!value.endsWith('.')){
value.append('.')
}
}
通过 where 关键字实现了上界约束的多个约束,每个约束中间需用 , 分割, 传递的参数 value 可以调用第 1 个约束 CharSequence 类中的 endsWith() 方法,同时也可以调用第 2 个约束 Appendable 类中的 append() 方法。
2 泛型约束<T : Any?> 与
<T : Any?> 表示类型实参是 Any 的子类,且类型实参可以为 null
表示类型实参是 Any 的子类,且类型实参不能为 null
//声明<T : Any?> 等同于 <T>
fun <T : Any?> nullAbleProcessor(value: T){
value?.hashCode()
}
fun <T : Any> nullDiasbleProcessor(value: T){
value.hashCode() //编译通过
}
fun main(args: Array<String>) {
nullAbleProcessor(null)
//nullDiasbleProcessor(null)//编译错误
//fun <T : Any> nullDiasbleProcessor(value: T): Unit
//is not satisfied: inferred type Nothing? is not a subtype of Any
}
6.3 子类和子类型
- 继承与子类型
如果 B 类是 A 类的子类,则 B 就是 A 的子类型。当新类的行为与父类完全一致,在任何使用父类的场合,新类都表现一致的行为,此时可以使用继承。
open class Animal{
fun eat(){
println("吃饭...")
}
}
class Cat : Animal()
fun work(animal : Animal) : Unit{
animal.eat()
}
fun main(args: Array<String>) {
var cat = Cat()
work(cat)//接收 animal 类型的参数,cat 继承 Animal 类
}
- 接口与子类型
如果 B 类实现了接口 A,则 B 类就是接口 A 的子类型,例如 String 类实现了 CharSequence 接口, String 类就是接口 CharSequence 的子类型。
fun export(str : CharSequence) : Unit{
println(str)
}
fun main(args: Array<String>) {
var str : String = "Hello Kotlin"
export(str)
}
- 可空类型的子类型
非空类型 String 是可空类型 String? 的子类型。来看代码验证
fun print(str : String?) : Unit{//需要传递可空参数
println(str)
}
fun main(args: Array<String>) {
var str1 : String = "非空"
var str2 : String? = null
print(str1)//非空参数
print(str2)//可空参数
//编译不报错也能正常运行,说明 String 类型是 String? 的子类型
}
fun print2(str : String) : Unit{//需要传递不可空参数
println(str)
}
//换成 print2 再次执行会报错,错误提示类型不匹配
注:
B 是 A 的子类型,但 Xxx< B> 不是 Xxx< A> 的子类型, Xxx 可以是一个类或接口,例如可以是 List 接口、PetShop 类等。如果 Cat 类是 Animal 类的子类,但 List< Cat> 并不是 List< Animal> 的子类型。
open class Animal{
fun bathe(){
println("开开心心的洗澡...")
}
}
class Cat : Animal() //猫类
class PetShop<T : Animal>(var animals : List<T>)//宠物店
//帮所有的宠物洗澡
fun bathAll(petShop : PetShop<Animal>){
for (animal : Animal in petShop.animals){
animal.bathe()
}
}
fun main(args: Array<String>) {
var cat1 = Cat() //第1只猫
var cat2 = Cat() //第2只猫
var animals = listOf<Cat>(cat1, cat2)
val petShop = PetShop<Cat>(animals)
//bathAll(petShop)//编译器报错 类型不匹配
}
6.4 协变与逆变
类或者接口上的泛型参数可以添加 out 和 in 关键字。对于泛型类型参数, out 关键字用于指定该类型参数是协变 Covariant, in 关键字用于指定该类型参数是逆变 Contravariance。 协变与逆变其实是 C# 语言 4.0 以后新增的高级特性,
协变是将父类变为具体子类,协变类型作为消费者,只能读取不能写入。
逆变是将子类变为具体父类,逆变作为生产者,只能写入不能读取。
- 协变
上面提到的 B 是 A 的子类型,默认情况下 Xxx< B> 不是 Xxx< A> 的子类型,可以通过 out 关键字使 Xxx< B> 是 Xxx< A> 的子类型,这样的操作叫作协变。
...
fun bathAll(petShop : PetShop<out Animal>){
for (animal : Animal in petShop.animals){
animal.bathe()
}
}
...
总结:
1、out 关键字只能出现在泛型类型或者泛型接口的泛型参数声明上,不能出现在泛型方法的泛型参数声明上。
2、out 关键字修饰泛型类或泛型接口的泛型参数时会支持协变。
- 逆变
通过关键字 in 可以使 Xxx< A> 不是 Xxx< B> 的子类型,这样的操作叫作逆变。
open class Animal
class Cat : Animal(){}
class Dog : Animal(){}
class PetShop<in T>{
fun feed(animal: T){
if(animal is Cat){
println("喂食小猫....")
}else if (animal is Dog){
println("喂食小狗....")
}
}
}
fun feedCat(petShop: PetShop<Cat>) : Unit{
petShop.feed(Cat())
}
fun main(args: Array<String>) {
feedCat(PetShop<Animal>())
//喂食小猫....
}
Cat 是 Animal 的子类型,由运行结果可知 PetShop 是 PetShop 子类型,这是在 PetShop 泛型参数上使用了 in 关键字, in 关键字是泛型参数产生了逆变。
总结:
1、in 关键字可以出现在泛型类型或者泛型接口的泛型参数声明上,不能出现在泛型方法的泛型参数声明上。2、in 关键字修饰泛型类或者泛型接口中的泛型参数时会支持逆变。
3、泛型参数 T 在使用了 in 关键字之后,不能声明成 val 或者 var 类型的变量。
- 点变形
上面说到的 out、in 关键字都是出现在类或者接口中的泛型参数声明的时候,这样做确实比较方便,因为它们的作用范围比较广,可以应用到所有类使用的地方。这种把 out、in 关键字放在泛型参数声明处的情况被称为声明点变形。注意,如果泛型参数中使用了 var 类型变量,则此处无法使用 out、in 关键字,也就不能声明点类型。 除了在类或接口中定义泛型参数时使用 out、in 关键字之外,还可在泛型参数出现的具体位置使用 out 、 in 关键字,这种变型被称为点变形。
open class Fruit(val name : String)
open class Mammal(val name: String)
class Banana : Fruit("香蕉")
class Pear : Fruit("梨子")
class Lion : Mammal("狮子")
class Tiger : Mammal("老虎")
class Forest<T>(var content: T)//使用了 var 类型变量,不能使用 in out
fun printFruit(forest: Forest<out Fruit>){//协变
println(forest.content.name)
}
fun printMamal(forest: Forest<out Mammal>){//协变
println(forest.content.name)
}
fun main(args: Array<String>) {
val bananaForest = Forest<Banana>(Banana())
val pearForest = Forest<Pear>(Pear())
val lionForest = Forest<Lion>(Lion())
val tigerForest = Forest<Tiger>(Tiger())
printFruit(bananaForest)
printFruit(pearForest)
printMamal(lionForest)
printMamal(tigerForest)
/*香蕉
梨子
狮子
老虎*/
}
6.5 泛型擦除与实化类型
- 泛型擦除
由于 JVM 虚拟机中没有泛型,因此泛型类的类型在编译时都会被擦除,所谓的擦除是指当定义一个泛型时,例如 List< String>类型,运行时只是 List,并不体现 String 类型。
fun main(args: Array<String>) {
val list1 = listOf("a", "b", "c")
val list2 = listOf(1, 2, 3)
println(list1.javaClass)//class java.util.Arrays$ArrayList
println(list2.javaClass)//class java.util.Arrays$ArrayList
println(list1.javaClass == list2.javaClass)//true 泛型已被擦除
}
- 泛型通配符
在 Java 中不知道泛型的具体类型时,使用通配符 ? 来替代具体类型
在 Kotlin 中使用 * 来替代具体类型,也被称为通配符,只能在 <> 中使用
- 星投影
当对泛型的实参一无所知时,仍然希望用安全的方式使用它时,可以使用星投影这种安全的方式。
星投影就是将泛型中的 * 等价于泛型中的注解 out 与 in 对应的协变类型参数与逆变类型参数,泛型的每个具体实例化将是该投影的子类型。
(1) 对于泛型类 A,其中 T 是一个具有上界 TUpper 的协变类型参数, A<*> 等价于 A< out TUpper>,这意味着当 T 未知时,可以安全地从 A< *> 中读取 TUpper 的值。
(2) 对于泛型类 A< in T>,其中 T 是一个逆变类型参数,A< *>等价于 A,由于 Nothing 没有任何值,因此意味着当 T 未知时,没有安全的方式写入 A< *>
(3) 对于泛型类 A< T>,其中 T 是一个具有上界 TUpper 的不型变类型参数, A< *>在读取是等价于 A< out TUpper>,而在写值时等价于 A< in Nothing>
如果泛型类型具有多个类型参数,则每个类型参数都可以进行单独的星投影。
例如,如果声明一个泛型类 B< in T,out U>,则可以根据星投影语法推测出一下星投影
如果泛型类为 B< *,String>,该泛型等价于 B<in Nothing,String>
如果泛型类为 B< Int,*>,该泛型等价于 B<Int,out Any?>
如果泛型类为 B< *, *>,该泛型等价于 B<in Nothing,out Any?>
- 实化类型
因为泛型在云心时会被擦除,要想知道某一个泛型形参在使用时具体是什么类型的泛型实参,在 Java 中,可以通过反射获取泛型的真实类型,在 Kotlin 中,可以通过在内联函数( inline) 中使用 reified 关键字修饰泛型参数即可,这样的参数称为实化类型。 reified 需要和 inline 一起使用,因为只有内联的泛型函数才可以在运行时获取泛型实参的类型。
inline fun <reified T> Any.isType() : Boolean{
if(this is T){
return true
}
return false
}
fun main(args: Array<String>) {
println("abc".isType<String>())
println(123.isType<String>())
//true
//false
}