Kotlin泛型、通配符、上限下限、协变逆变、PECS、out T、in T、类型擦除

Kotlin从入门到进阶实战

为何引入泛型

为何引入泛型,最引人注意的一个原因是为了创建容器类。集合类可以说是最常用的类之一,在没有泛型前,集合类是怎样持有对象的呢。在java中Object是所有类的根类。为了集合类的通用性,把元素类型定义为Object,当放入具体类型时,再进行相应的强制类型转换。在使用原生态类型实现的集合类中,使用Object[]数组。这种方式常见的问题有两个:
1.像集合中添加元素的时候没有对元素类型进行检查。
2.从集合中取元素的时候,不能都使用Object类型,需要进行强制类型转换。而转换过程由于添加的时候没有任何限值和检查,所以容易出错。(java.lang.ClassCastException):java.lang.Integer cannot be cast to java.lang.string
对于这样的代码,编译器不会报错,但是运行时可能会发生类型转换错误。
泛型最主要的优点就是让编译器追踪参数类型。执行检查和类型转换。

在类、接口和函数上使用泛型
  • 泛型接口
interface MyInterface<T> {//类型参数放在接口名称后面:<T>
    operator fun next(): T //接口参数中直接使用类型T
}

使用如下,使用object关键字声明一个MyInterface实现类,并调用next()函数

fun test() {
        val inter = object : MyInterface<Int> {
            override fun next(): Int {
                return Random().nextInt(100)
            }
        }
        println(inter.next())
    }

Kotlin中Map和MutableMap接口的定义也是一个典型的泛型接口的例子。

public interface Map<K, out V> {
	//.........
    public fun containsKey(key: K): Boolean
    public fun containsValue(value: @UnsafeVariance V): Boolean
    public operator fun get(key: K): V?
    @SinceKotlin("1.1")
    @PlatformDependent
    public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
        // See default implementation in JDK sources
        return null as V
    }
    public val keys: Set<K>
    public val values: Collection<V>
    public val entries: Set<Map.Entry<K, V>>
    public interface Entry<out K, out V> {
        /**
         * Returns the key of this key/value pair.
         */
        public val key: K

        /**
         * Returns the value of this key/value pair.
         */
        public val value: V
    }
}

使用mutableMapOf初始化一个Map

mutableMapOf<Int, String>(1 to "A", 2 to "B", 3 to "C")

mutableMapOf函数如下:

public fun <K, V> mutableMapOf(vararg pairs: Pair<K, V>): MutableMap<K, V>
        = LinkedHashMap<K, V>(mapCapacity(pairs.size)).apply { putAll(pairs) }

这里的K、V在泛型类型被实例化和使用时,将被实际的类型所替代。
泛型可以用来限制集合类持有的对象类型,这样使得类型更加安全。我们在集合里放入错误的类型编译器会报错。
Kotlin中有类型推断功能,有些类型参数可以省略不写。

mutableMapOf(1 to "A", 2 to "B", 3 to "C")
  • 泛型类
    直接声明一个带类型参数的Container类
class Container<K, V>(var key: K, var value: V) {
    override fun toString(): String {
        return "key = $key  value = $value"
    }
}

测试

 fun test() {
        val container = Container<Int, String>(1, "A")
        println(container)
    }

输出如下

key = 1  value = A
  • 泛型函数
    函数中使用泛型代码如下
 fun <T> test(t: T) {
        println(t)
    }

    interface MyInter {
        fun <T> go(t: T)
    }

    fun <T : Comparable<T>> gt(x: T, y: T): Boolean {
        return x > y
    }
类型上界

在下面这函数中

 fun <T : Comparable<T>> gt(x: T, y: T): Boolean {
        return x > y
    }

T : Comparable ,这里的Comparable是类型T的上界。也就是说类型T代表的都是实现了Comparable接口的类。这样等于告诉编译器它们都是事先了CompareTo方法。如果没有声明上界,就无法使用>操作。

协变与逆变

我们来看一下问题场景。有下面存在子父类关系的类型。

open class Food
open class Fruit : Food()
class Apple : Fruit()
class Banana : Fruit()
class Grape : Fruit()

然后有下面的两个函数

object Text {

    fun addFruit(fruit: MutableList<Fruit>) {

    }

    fun getFruit(fruit: MutableList<Fruit>) {

    }

当向里面存放Apple的时候

   val apples = mutableListOf(Apple(), Apple(), Apple())
    addFruit(apples)//报错 Type mismatch

在这里插入图片描述
如果没有协变,我们要在里面添加两个方法

 fun addFruit(fruit: MutableList<Fruit>) {

    }

    fun getFruit(fruit: MutableList<Fruit>) {

    }

    fun addApple(apple: MutableList<Apple>) {

    }

    fun getApple(apple: MutableList<Apple>) {

    }

这样是重复的样板代码,那么怎么避免这个问题呢,让MutableList成为其父类型。java泛型中引入了类型通配符的概念来解决这种问题。java泛型通配符大概有两种方式:

  1. 子类型上界通配符 ? extends T 指定类型参数的上限,该类型必须为类型T或者它的子类型。也就是说 MutableList<?extends Fruit> 是MutableList的父类型。
  2. 超类型下界限定符 ?superT 指定类型参数的下限。该类型必须是T或者它的父类型,也就是说MutableList<?super Fruit>是MutableList的父类型。

这里的 ? ,称之为类型通配符。它们为一个泛型类所指定的类型集合提供了一个有用的类型范围。

  • Number类型为F Integer类型是 C ,F 是 C 的父类型,我们把这种父子类型关系记为C=>F C继承F ;而List代表的泛型类型信息为f(F) 、 f©。我们可以这样描述协变和逆变。
  1. 当C=>F时,如果有f© => f©,那么 f 叫做协变
  2. 当C=>F时,如果f(F) => f(F),那么 f 叫做逆变。如果这两种关系都不成立,则叫做不变。
  3. 协变和逆变都是类型安全的。

Kotlin中用 out 和 in 代表 extends 和 super 所以上面的代码可以这么写 编译器就不会报错了。

object Text {

    fun addFruit(fruit: MutableList<out Fruit>) {

    }

    fun getFruit(fruit: MutableList<out Fruit>) {

    }


    @JvmStatic
    fun main(args: Array<String>) {
        val apples = mutableListOf(Apple(), Apple(), Apple())
        addFruit(apples)
    }
}
协变
  • va中数组是协变的,下面代码可以正确运行
public static void main(String[] args) {
        Integer[] integers = new Integer[3];
        integers[0] = 0;
        integers[1] = 1;
        integers[2] = 2;
        Number[] numbers = new Number[3];
        numbers = integers;
        for (Number number :numbers){
            System.out.println(number);
        }
    }

在java中Integer是Number子类型,数组Integer[]也是Number[]子类型,因此任何需要Number[]值的地方都可以提供一个Integer[]值。所以说 java中数组是协变的。
在这里插入图片描述

  • java中泛型是非协变的,也就是说List< Integer >不是List< Number > 的子类型
    在这里插入图片描述
    在这里插入图片描述

我们使用通配符写法如下:
在这里插入图片描述

同样是报错的。为什么Number对象可以由Integer实例化。而List< Number >不能由List< Integer >实例化呢?

List<? extends  Number> integers =new ArrayList<>();

这里的类型C是 Number或者其子类(Number Integer Float等)。父类F就是上界通配符?extends Number。但是这里不能像 integers 里面添加除了 null 以外的对象。

 List<? extends Number> integers = new ArrayList<Integer>();
        integers.add(null);//成功
        integers.add(new Integer(1));//报错

如果能添加List< Number>的子类,也可以添加List< Integer>的子类,那么集合中的元素类型会比较混乱。我们无法判断哪个类型是Integer 哪个类型是Float,为了保持类型一致,java禁止像List<?extends Number>中添加任意Number类型的对象。不过可以添加null。

逆变
List<? super Number> numbers = new ArrayList<Object>();

这里的子类型C是“?super Number”,父类型F是Number的父类型,逆变的意义如下
在这里插入图片描述

public static void main(String[] args) {
        List<? super Number> list = new ArrayList<Number>();
        List<? super Number> list1 = new ArrayList<Object>();
        list.add(new Integer(3));
        list1.add(new Integer(4));
    }

在逆变类型中,我们可以向其中添加元素。例如我们可以向List< ? super Number> list中添加Number以及子类对象

PECS(Producer-Extends,Consumer-Super)

什么时候使用 extends 什么时候使用 super 呢,在java.util.Collections的copy方法中诠释了PECS。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next()T
                di.set(si.next());
            }
        }
    }

上界<? extends T>不能往里存,只能往外取
下界<? super T>可以往里存,但往外取只能放在Object对象里

out T 、in T
  • Kotlin抛弃了通配符?,直接实现了PECS的规则
  • out T 等价于 ?extends T
  • in T等价于 ? super T
类型擦除
  • java和Kotlin的泛型实现,都是采用了运行时类型擦除的方式。也就是说在运行时,这些类型的参数信息会被擦除。
    泛型是在编译器层次上实现的,生成的class字节码是不包含泛型中的类型信息的。例如在代码中定义的List< String> JVM看到的只是 List,由泛型添加的信息 JVM是不可见的。
  • 泛型很多特性都与这个类型擦除有关,比如List< String>.class 不存在,只有List.class ,对应的Kotlin中也只有 MutableList::class。
  • 类型擦除的过程
    (1) 首先找到用来替换类型参数的具体类。一般是Object。如果指定了类型上界的话就使用这个上界。
    (2) 其次,把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明。即去掉< >的内容。
    (3) 最后根据需要生成一些桥接方法。这是由于擦除了类型之后的类可能缺少某些必须的方法。这个时候由编译器来动态生成。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值