Kotlin 进阶之路(六) 泛型

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
}

后续

Kotlin 进阶之路(七) Gradle 写作中…

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cczhengv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值