Kotlin学习笔记 - 泛型

1. 基本用法

class Box {

    private var element: T? = null

    fun add(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

2. 型变

型变包括 协变、逆变、不变 三种:

  • 协变:泛型类型与实参的继承关系相同
  • 逆变:泛型类型与实参的继承关系相反
  • 不变:泛型类型与实参类型相同

Java的型变

首先明确一点,Java不直接支持型变。通俗地讲,虽然Integer是Number的子类,但是在Java中,List和List是没有关系的(List不是List的子类)所以List不能直接赋值给List

如果Java支持型变,则会出现以下情况:

// error:以下代码实际上会编译报错
List numList = new ArrayList();
// 假设以上代码能够编译通过,以下代码就会在运行时引发异常
// numList实际上操作的集合元素必须是Integer类型
numList.add("haha"); // ClassCastException

Java采用通配符的方式来处理型变的需要(泛型通配符)

public interface Collection extends Iterable {
    // Java泛型通配符上限(协变), 可用于接收 E 或 E 的子类型
    boolean addAll(Collection c);
    
    // Java泛型通配符下限(逆变), 可用于接收 E 或 E 的父类型
    boolean removeIf(Predicate filter);
}

以上是泛型通配符在Collection接口源码中的使用

  • 通配符上限
public static void test(List list) {
    /*
     * 对于“通配符上限”语法而言,从该集合中取出元素是安全的。
     * 集合中的元素是Number或其子类型,但是不能往该集合中存入新的元素,
     * 因为无法预测该集合元素的实际类型是Integer还是Double,又或者是Number的其他子类型
     */
    for (Number num : list) {
        System.out.println(num);
    }
    // list.add() // 不能添加元素,因为不能确定元素的类型
}
  • 通配符下限
public static void test(List list) {
    /*
     * 对于"通配符下限"语法而言,将对象传给泛型对象是安全的。
     * 该集合中的元素是Integer或其父类型,但是从该集合中取出元素是不安全,
     * 因为无法预测该集合元素的实际类型是Number还是Object
     */
    list.add(666);
    // 以下代码编译不通过,因为在取出元素时无法确认元素的类型,可能是Number类型,也可能是Object类型
    // Number number = list.get(0);
}

泛型的规律

  1. 通配符上限(泛型协变)意味着从中取出(out)对象是安全的,但传入对象是不安全的
  2. 通配符下限(泛型逆变)意味着向其中传入(in)对象是安全的,但取出对象是不安全的

Kotlin的型变

Kotlin处理泛型型变的规则就是根据泛型的规律而设计:

  1. 如果泛型只需要出现在方法的返回值申明中(不出现在形参的声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限);如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰。
  2. 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限);如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰。

声明处型变

  • 如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰
class Box {
    private var element: T? = null
    
    fun get(): T? = element
}

fun main(args: Array) {
    // 由于泛型使用out修饰,所以 Box 对象可以直接赋值给 Box
    var box: Box = Box()
}
  • 如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰
class Box {
    private var element: T? = null

    fun put(e: T) {
        this.element = element
    }
}

fun main(args: Array) {
    // 由于泛型使用in修饰,所以 Box 对象可以直接赋值给 Box
    var box: Box = Box()
}

声明处型变的限制:
如果一个类中有的方法使用泛型声明返回值类型,有的方法使用泛型声明形参类型,那么该类就不能使用声明处型变。

使用处型变

class Box {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array) {
    // 使用 out 修饰泛型
    var outBox: Box = Box()
    // 不能调用put方法存入数据(编译报错)
    // outBox.put(18)
    val num: Number? = outBox.get()

    // 使用 in 修饰泛型
    var inBox: Box = Box()
    inBox.put(18)
    // 不能确定返回值的类型,只能使用Any类型接收
    val e: Any? = inBox.get()
}

@UnsafeVariance注解

对于协变的类型,通常是不允许将泛型类型作为方法参数的类型,但是在某些情况下,我们需要在协变的情况下将泛型作为方法的参数类型,那么我们可以使用 @UnsafeVariance 注解来修饰泛型。

class Box {
    private var element: T? = null

    // 如果开发者自己可以保证类型安全,那么可以使用@UnsafeVariance注解让编译器不报错
    fun put(element: @UnsafeVariance T) {
        this.element = element
    }

    fun get(): T? = element
}

星投影

在Java中,当我们不确定泛型的具体类型是,可以使用 ? 来代替具体的泛型,比如

List list = new ArrayList();

在Kotlin也有类似的语法,可以使用 * 来指代相应的泛型映射

// 星投影
val list: List<*> = ArrayList()

以下是官方对于星投影语法的解释:

  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,你可以安全地从 Foo <> 读取 TUpper 的值。

  • 对于 Foo ,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <>。

  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo 而对于写值时等价于 Foo

简而言之,星投影就是:
  • 当 * 接收可协变的泛型参数 ( out T ) 时,* 映射的类型为 Any?
class Box {
    private var element: T? = null

    fun get(): T? = element
}

fun main(args: Array) {
    val intBox: Box = Box()

    val numBox: Box = intBox
    val num: Number? = numBox.get()

    // 星投影,这里的 <*> 相当于 ,元素类型映射为 Any?
    val box: Box<*> = intBox
    val element: Any? = box.get()
}
  • 当 * 接收可逆变的泛型参数 ( in T ) 时,* 映射的类型为 Nothing
class Box {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }
}

fun main(args: Array) {
    // 星投影,这里的 <*> 相当于 ,元素类型映射为 Nothing
    val box: Box<*> = Box()
    box.put(/*element:Nothing*/) // 没有值可以传入
}
  • 当 * 接收不变的泛型参数 ( T ) 时,* 对于读取值类型映射为 Any?,而写值时类型映射为 Nothing
lass Box {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array) {
    // 星投影
    val box: Box<*> = Box()
    // 这里的 <*> 在赋值时相当于 ,元素类型映射为 Nothing
    box.put(/*element:Nothing*/) // 没有值可以传入
    // 这里的 <*> 在取值时相当于 ,元素类型映射为 Any?
    val element: Any? = box.get()
}

3. 泛型函数

泛型函数就是在函数声明时定义一个或多个泛型,泛型的声明必须在 fun 与函数名之间

fun  test(a: T) {
    println(a)
}

fun main(args: Array) {
    test(1) // 可省略,类型通过参数自动推断
    test(2)
}

泛型函数也可以用于扩展函数

fun  T.test(): String {
    return "test(): ${this.toString()}"
}

fun main(args: Array) {
    val num = 666
    // 显示指定泛型为 Int 类型,可省略
    println(num.test())

    // 不显示指定泛型的类型,编译器自动推断出泛型为 String 类型
    val str = "haha"
    println(str.test())
}

4. 具体化类型参数

Kotlin允许在内联函数中使用 reified 修饰泛型参数,这样就可以将该泛型参数变成一个具体化的类型参数。具体化类型参数后就可以在函数中将泛型当做一个普通类型来使用,比如可以使用 is、as 运算符。

比如,我们需要在list中找到指定类型的元素,原先的写法如下

fun  findData(list: List<*>, clazz: Class): T? {
    for (e in list) {
        if (clazz.isInstance(e)) {
            @Suppress("UNCHECKED_CAST")
            return e as T
        }
    }
    return null
}

fun main(args: Array) {
    val list = listOf(1, 6.6f, "haha")
    println(findData(list, String::class.java)) // haha
    println(findData(list, Float::class.javaObjectType)) // 6.6
}

使用具体化类型参数之后,代码如下:

// 很明显,代码变得更简洁了
inline fun  findData(list: List<*>): T? {
    for (e in list) {
        if (e is T) {
            return e
        }
    }
    return null
}

fun main(args: Array) {
    val list = listOf(1, 6.6f, "haha")
    println(findData(list))
    println(findData(list))
}

使用reified修饰的泛型参数,还可以对其使用反射

inline fun  test() {
    println(T::class.java)
}

fun main(args: Array) {
    test() // class java.lang.String
    test() // class java.lang.Double
}

5. 泛型边界(上界)

java中可以使用 extends 指定泛型的上界

class Box {
    // ...
}

Koltin的实现:

class Box {
    // ...
}

如果需要指定多个边界,需要使用where子句(只能有一个父类上界,可以有多个接口上界)

class Box where T : Number, T : Comparable {
    // ...
}

PS:在Kotlin中,如果不指定边界,则默认边界是 Any#

你可能感兴趣的:(Kotlin学习笔记 - 泛型)