文章

Kotlin高级——泛型

开始

泛型类

1
2
3
class MyClass<T> {
    fun method(t: T): T
}

泛型函数

1
2
3
fun <T> method(t: T): T {
    // statements
}

泛型约束

用于约束泛型参数的上界,类似java的extends,默认的上界是Any?

1
class Person<T : String>

当类型需要多个上界时,使用where语句,T类型必须同时继承/实现A类型和B类型

1
class Person<T> where T : A, T : B

类型擦除

在Java和Kotlin中都有类型擦除,指的是在使用泛型时,泛型参数会在编译期间去除,也就是List<T>的泛型参数T会被擦除,List<T>就变成List,类中使用到泛型参数的地方都会被赋予类型

随之带来一个问题,就是无法在运行时判断泛型参数的类型(Kotlin可以通过reified关键字实现),同时无法描述泛型参数的类型关系,其中一个例子是List<Object>不是List<String>的父类,这样的现象称为不变

协变与逆变

协变和逆变用于解决不变问题,二者统称为型变

协变和逆变通常用在函数参数和返回值的类型声明上,用于扩展容器输入和输出的类型范围,并不能进行实际地转换,如果要实现类型转换,只能创建新对象,手动进行转换

星投影:类似java的泛型通配符?,用于接收不确定的泛型信息,只能读取值(向外输出值),相当于<out Any?>

  • Java实现:使用extends实现协变,使用super实现逆变,只支持使用处型变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    interface Collection<T> {
        // 协变:向下扩展类型范围,只能安全读取,确保读取的都是T的子类
        Collection<? extends T> method1();
        void method2(Collection<? extends T> c);
            
        // 逆变:向上扩展类型范围,只能安全写入,确保写入的都是T的子类
        Collection<? super T> method3();
        void method4(Collection<? super T> c);
    }
    
  • Kotlin实现:使用out实现协变,使用in实现逆变,支持声明处型变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // 使用处型变
    interface Collection<T> {
        // 协变
        fun method1(): Collection<out T>;
        fun method2(c: Collection<out T>);
            
        // 逆变
        fun method3(): Collection<in T>;
        fun method4(c: Collection<in T>);
    }
        
    // 声明处型变
    interface Collection<out T> {
        // T只支持协变
        fun method1(): Collection<T>;
        fun method2(c: Collection<T>);
    }
    interface Collection<in T> {
        // T只支持逆变
        fun method3(): Collection<in T>;
        fun method4(c: Collection<in T>);
    }
    

协变逆变原理

核心原理为一个父类引用可以无障碍地接收一个子类对象,协变和逆变都是基于这一点实现的

容器泛型使用协变还是逆变,与它作为形参还是返回值是无关的,要看该容器的值的使用场景

  • 当容器在消费场景时,容器需要被读取,因此外部需要使用一个父类引用接收读取值,因此容器泛型使用协变声明,确保容器内的值都是外部声明类型的子类,可以安全接收
  • 当容器在生产场景时,容器需要被写入,因此容器内需要使用一个父类引用接收写入值,因此容器泛型使用逆变声明,确保写入值都是容器声明类型的子类,可以安全写入

具体化泛型参数

用于解决获取泛型参数的运行时类型问题

获取泛型的运行时类型场景

  • 序列化和反序列化
  • 数据库操作
  • 自定义注解
  • 泛型类型检查

kotlin和java在运行时会进行类型擦除,因此无法在运行时获取泛型的实际类型

  • java可以通过反射获取
  • kotlin使用reified+inline实现
1
2
3
4
5
6
7
8
9
// inline+reified
inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    // T 可以作为类型,用于检查和转换
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}
本文由作者按照 CC BY 4.0 进行授权