「码上开学——hencoder」Kotlin笔记(泛型)

388 阅读12分钟

从Kotlin的in和out说起

提到Kotlin的泛型,通常离不开inout关键字

下面这段Java代码在日常开发中应该很常见了:

List<TextView> textViews = new ArrayList<TextView>();

其中List<TextView表示这是一个泛型类型为TextViewList

那到底什么是泛型呢?

现在的程序开发的大都是面向对象的,平时会用到各种类型的对象,一组对象通常要用集合来存储它们,因而就有了一些集合类,比如ListMap等。

这些集合类里面都是装的具体类型的对象,如果每个类型都去实现诸如TextViewListActivityList这样的具体的类型,显然是不可能的。

因此就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再确定它的类型。

前面那个例子,List<TextView>就是泛型类型声明。

既然泛型是跟类型相关的,那么是不是也能使用类型的多态呢?

先看一个常见的使用场景:

TextView textView = new Button(context);
// 👆🏻这是多态

List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = button;
//👆🏻多态用在这里会报错,incompatible types: List<Button> cannot be converted to List<TextView>

我们知道Button是继承自TextView的,根据Java多态的特性,第一处赋值是正确的。

但是到了List<TextView>的时候IDE就报错了,这是因为Java的泛型本身具有「不可变性Invariance」,Java里面认为List<TextView>List<Button>类型并不一致,也就是说,子类型的泛型(List<Button>)不属于泛型(List<TextView>)的子类。

Java的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。 但是换成数组就没有在编译时擦除类型。

但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。

Java提供了「泛型通配符」 ? extends? super来解决这个问题。

Java中的? extends

在Java里面是这么解决的:

List<Button> buttons = new ArrayList<Button>();

List<? extends TextView> textViews = buttons;

这个? extends叫做「上界通配符」,可以使Java泛型具有「协变性Convariance」,协变就是允许上面的赋值是合理的。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。`extends`限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  • 其中?是个通配符,表示这个List的泛型类型是一个未知类型
  • extends限制了这个未知类型的上界,也就是泛型类型必须满足这个extends的限制条件,这里和定义classextends关键字有点不一样:
    • 它的范围不仅是所有直接或间接子类,还包括上界定义的父类本身,也就是TextView.
    • 它还有implements的意思,即这里的上界也可以是interface

这里ButtonTextView的子类,满足了泛型类型的限制条件,因而能够成功赋值。

根据刚才的描述,下面几种情况都是可以的:

List<? extends TextView> textViews = new ArrayList<TextView>();// 本身
List<? extends TextView> textViews = new ArrayList<Button>();// 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>();//间接子类

一般集合类都包含了getadd两种操作,比如Java中的List,它的具体定义如下:

public interface List<E? extends Collection<E>{
    E get(int index);
    boolean add(E e);
    ...
}

上面的代码中,E就是表示泛型类型的符号(用其他字母甚至单词都可以).

我们看看在使用了上界通配符之后,List的使用上有没有什么问题:

List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0);// get可以

textViews.add(textView);
//add 会报错,no suitable method found for add(TextView)

前面说到List<? extends TextView>的泛型类型是个未知类型?,编辑器也不确定它是啥类型,只是有个限制条件。

由于它满足? extends TextView的限制条件,所以get出来的对象,肯定是TextView的子类,根据多态的特性,能够赋值给TextView,啰嗦一句,赋值给View也是没有问题的。

add操作的时候,我们可以这么理解:

  • List<? extends TextView>由于类型未知,它可能是List<Button>,也可能是List<TextView>
  • 对于前者,显然我们要添加TextView是不可以的。
  • 实际情况是编辑器无法确定到底属于哪一种,无法继续执行下去,就报错了。

要我干脆不要extends TextView,只用通配符?呢? 这样使用List<?>其实是List<? exteds Object的缩写。

List<Button> buttons = new ArrayList<>();

List<?> list = buttons;
Object obj = list.get(0);

list.add(obj);这里还是会报错

和前面的例子一样,编辑器没法确定?的类型,所以这里就只能getObject对象。

同时编辑器为了保证类型安全,也不能向List<?>中添加任何类型的对象,理由同上。

由于add的这个限制,使用了? extends泛型通配符List,只能够向外提供数据类型被消费,从这个角度来讲,向外提供数据的一方称为「生产者Producer」。对应的还有一个概念叫「消费者Consumer」,对应Java里面另一个泛型通配符? super

Java中的?super

List<? super Button> buttons = new ArrayList<TextView>();

这的? super叫做「下界通配符」,可以使用Java泛型具有「逆变性Contravariance」.

与上界通配符对应,这里super限制了通配符?的子类型,所以称之为下界。

它也有两层意思:

  • 通配符?表示List的泛型类型是一个未知类型
  • super限制了这个未知类型的下界,也就是泛型必须满足这个super的限制条件。
    • super我们在类的方法里面经常用到,这里的范围不仅包括Button的直接和间接父类,也包括下界Button本身。
    • super同样支持interface

上面的例子中,TextViewButton的父类型,也就能够满足super的限制条件,就可以成功赋值了。

根据刚才的描述,下面几种情况都是可以的:

List<? super Button> buttons = new ArrayList<Button>();// 本身
List<? super Button> buttons = new ArrayList<TextView>();//直接父类
List<? super Button> buttons = new ArrayList<Object>();//间接父类

对于使用了下界通配符的List,我们再看看它的getadd操作:

List<? super Button> buttons = new ArrayList<TextView>();
Object object = button.get(0);// get出来的是Object类型
Button button = ...
buttons.add(button);//add 操作是可以的

解释下,首先?表示未知类型,编译器是不确定它的类型的。

虽然不知道他的具体类型,不过在Java里任何对象都是Object的子类,所以这里能把它赋值给Object

Button对象一定是这个未知类型的子类,根据多态的特性,这里通过add添加Button对象是合法的。

使用下界通配符? super的泛型List。只能读取到Object对象,一般没有什么实际的使用场景,通常也只拿来添加数据,也就是消费已有的List<? super Button>,往里面添加Button,因此这种泛型类型声明称之为「消费之Consumer」。


小结下,Java的泛型本身是不支持协变和逆变的。

  • 可以使用泛型通配符? extends来使泛型支持协变,但是「只能去读不能修改」,这里的修改仅指对泛型集合添加元素,如果是remove(int index)以及clear当然是可以的
  • 可以使用泛型通配符? super来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照Object读出来再强转当然也是可以的。

根据前面的说法,这被称为PECS法则:「Producer-Extends, Consumer-Super」。

理解了Java的泛型之后,再理解Kotlin中的泛型,容易多了。

说回Kotlin中的out和in

和Java泛型一样,Kotlin中的泛型本身也是不可变的。

  • 使用关键字out来支持协变,等同于Java中的上界通配符? extends
  • 使用关键字in来支持逆变,等同于Java中的下界通配符? super
var textView: List<out TextView>
var textView: List<in TextView>

换了个写法,但作用是完全一样的。out表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。

你看,我们Android工程师学不会outin,其实并不是因为这两个关键字多难,而是因为我们应该先学学Java的泛型。

说了这么多List,其实泛型在非集合类的使用也非常广泛,就以「生产者-消费者」为例子:

class Produver<T> {
    fun produce(): T {
        ...
    }
}

val producer: Producer<out Text> = Producer<Button>()
val textView: TextView = produver.produce()// 相当于get

再来看看消费者:

class Consumer<T> {
    fun consume(t: T) {
        ...
    }
}

val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context))// 相当于add

声明处的out和in

在前面的例子中,在声明Producer的时候已经确定了泛型T只会作为输出来用,但是每次都需要在使用的时候加上out TextView来支持协变,写起来很麻烦。

Kotin提供了另外一种方法:可以在声明类的时候,给泛型符号加上out关键字,表明泛型参数T只会用来输出,在使用的时候就不用额外的out了。

class Producer<out T> {
    fun produce(): T {
        ...
    }
}

val producer: Producer<TextView> = Producer<Button>()//这里不写out 也不会报错
val producer: Producer<out TextView> = Producer<Button>()// out可以但是没必要

out一样,可以在声明类的时候,给泛型参数加上in关键字,来表明这个泛型参数T只用来输入。

class Consummer<in T> {
    fun consume(t: T) {
        ...
    }
}

val consumer: Consumer<Button> = Consumer<TextView>()// 这里不写in也不会报错
val consumer: Consumer<in Button> = Consumer<TextView>()//in可以但没必要

* 号

前面讲到了Java中单个?号也能作为泛型通配符使用,相当于? extends Object。 它在Kotlin中有等效的写法:*号,相当于out Any

vat list: List<*>

和Java不同的地方是,如果你的类型定义里已经有了out或者in,那这个限制在变量声明时也依然在,不会被*号去掉。

比如你的类型定义里是out T: Number的,那它加上<*>之后的效果就不是out Any,而是out Number

where关键字

Java中声明类或接口的时候,可以使用extends来设置边界,将泛型类型参数限制为某个类型的子集:

// T的类型必须是Animal的子类型
class Monster<T extends Animal> {
}

注意这个和前面讲的声明变量时的泛型类型声明时不同的东西,这里并没有?

同时这个边界是可以设置多个,用&符号连接:

// T 的类型必须同时是Animal和Food的子类型
class Monster<T extends Animal & Food> {
}

Kotlin只是把extends换成了:冒号。

class Monster<T: Animal>

设置多个边界可以使用where关键字:

class Monster<T> where T: Animal, T: Food

有人在看文档的时候觉得这个where是个新东西,其实虽然Java里没有where,但它并没有带来新功能,只能把一个老公能换了个新写法。

Kotlin里where这样的写法可读性更符合英文里的语法,尤其是如果Monster本身还有继承的时候

class Monster<T>: MonsterParent<T> where T: Animal

reified关键字

由于Java中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。

比如你不能检查一个对象是否为泛型类型T的实例:

<T> void printIfTypeMatch(Object item) {
    if (item instanceof T) {// IDE 会提示错误,illegal generic type for instanceof
        System.out.println(item);
    }
}

Kotlin里同样也不行:

fun <T> printIfTypeMatch(item: Any) {
    if (item is T) {// IDE 会提示错误, Cannot check for instance of earsed type: T
        println(item)
    }
}

这个问题,在Java中的解决方案通常是额外传送一个Class<T>类型的参数,然后通过Class#isInstance方法来检查:

<T> void check(Object item, Class<T> type) {
    if (type.isInstance(item)) {
        System.out.println(item);
    }
}

Kotlin中同样可以这么解决,不过还有一个更方便的做法:使用关键字reified配合inline来解决:

inline fun <reified T> printIfTypeMatch(item: Any) {
    if (item is T) {//这里就不会再提示错误了
        println(item)
    }
}

Kotlin泛型与Java泛型不一致的地方

  1. Java里的数组是支持协变的,而Kotlin中的数组Array不支持协变,因为在Kotlin中数组是用Array类来表示的,这个Array类使用泛型几个集合一样,所以不支持协变。
  2. Java中的List接口不支持协变,而Kotlin中的List接口支持协变。Java中的List不支持协变,需要使用泛型通配符来解决。 在Kotlin中,实际上MutableList接口才相当于List。Kotlin中的List接口实现了只读操作,没有写操作,所以不会有类型安全事实上的问题,自然可以支持协变。

练习题

  1. 实现一个 fill 函数,传入一个 Array 和一个对象,将对象填充到 Array 中,要求 Array 参数的泛型支持逆变(假设 Array size 为 1)。
fun <T> fill(array: Array<in T>, obj: T) {
    array[0] = obj
}

fun main() {
    val obj = Any()
    val array = Array<Any?>(1)
    fill(array, obj)
    println(array[0])
    
}

上述实例中,fill函数使用泛型的方法,并接受一个Array<in T>类型的参数和一个泛型对象T。这里使用了in关键字来实现逆变,表示可以接受T的超类型,在fill函数内部,将对象obj设置到Array的指定位置上。

需要注意的是,由于Kotlin中的数组是不变型的,因此无法直接创建一个协变或逆变的数组。在示例上,使用Array<Any?>(1)来创建一个可接受任何类型的数组,并将其作为逆变数组类型的参数传递给fill函数。

2.实现一个 copy 函数,传入两个 Array 参数,将一个 Array 中的元素复制到另外个 Array 中,要求 Array 参数的泛型分别支持协变和逆变。(提示:Kotlin 中的 for 循环如果要用索引,需要使用 Array.indices

fun <T> copy(source: Array<out T>, destination: Array<in T>) {
    for (index in source.indices) {
        destination[index] = source[index]
    }
}

fun main() {
    val sourceArray = arrayOf("apple", "banana", "orage")
    val destinationArray = arrayOfNulls<Any>(3)
    copy(sourceArray, destinationArray)
    println(destinationArray.joinToString())// 输出:apple, banana, orange
}

上述示例中,copy函数使用了泛型方法,并接受一个协变的Array<out T>类型参数的source和一个逆变的Array<in T>类型参数destination。在copy函数内部,使用for循环遍历source.indices,并将source中的元素赋值到destination中相应的位置。

需要注意的是,Array.indices返回一个IntRanger对象,表示索引的范围。正在for循环中使用source.indices可以迭代数组的索引,然后使用索引访问和操作数组元素。

在示例中,sourceArray是一个协变的数组,可以接受其子类型的数组作为参数,而destinationArray是一个逆变的数组,可以接受其超类型的数组作为参数。这样就实现了协变和逆变的数组之间进行元素复制的需求。

版权声明

本文首发于:rengwuxian.com/kotlin-basi…

微信公众号:扔物线