阅读 597

Kotlin知识归纳(十二) —— 泛型

Java为什么引入泛型

      众所周知,Java 5才最大的亮点就是引入泛型,那么Java引入泛型的目的是什么?这就需要查看Java 5引入泛型前的代码:(因为Java向后兼容,现在这段代码还能编译成功)

#daqiJava.java
List list = new ArrayList();
list.add("");
String str = (String) list.get(0);
//添加错误类型
list.add(1);
复制代码

      由于ArrayList底层是依靠Object数组实现的,这使得任何类型都可以添加到同一个ArrayList对象中。且取出来时是Object类型,需要强制类型转换后才能进行相应的操作。但由于ArrayList对象能接受任何类型,无法保证类型转换总是正确的,很容易造成ClassCastException异常。

      但泛型的出现,让这一切都迎刃而解。单个ArrayList对象只能存储特定类型的对象,如果不是存入该类型或者该类型子类的对象,编译器会报错提醒,规范了ArrayList中对象的类型。同时,取出来时可以安心的依据泛型的具体类型进行强制类型转换,并且这是在ArrayList中自动完成强转的,省去了开发者进行强制类型转换带来的繁琐。

#daqiJava.java
List<String> list = new ArrayList();
list.add("");
String str = list.get(0);

list.add(1);//编译器不通过
复制代码

总的来说,泛型带来以下好处:

  • 在编译期检查类型,让类型更安全
  • 自动类型转换
  • 提高代码通用性

类型参数约束

上界

类型参数约束可以限制作为泛型类和泛型函数的类型实参的类型。

      把一个类型指定为泛型的类型形参的上界约束,在泛型类型具体的初始化中,对应的类型实参必须是这个具体类型或它的子类型。

      换句话说就是,某泛型函数(例如求和函数)可以用在List<Int>List<Double>上,但不可以用在List<String>上。这时可以指定泛型类型型参的上界为Number,使类型参数必须使数字。

fun <T:Number> sum(num1:T,num2:T):T{
    
}
复制代码

一旦指定上界,只有 Number 的子类(子类型)可以替代 T。

尖括号中只能指定一个上界,如果同一类型参数需要多个上界,需要使用 where-子句

fun <T> daqi(list: List<T>) 
    where T : CharSequence,T : Comparable<T> {
    
}
复制代码

类型形参非空

      类型参数约束默认的上界是 Any?。意味着泛型函数接收的参数可空,尽管泛型T并没有标记? 。这时可以使用<T : Any>替换默认上界,确保泛型T永远为非空类型。

类、类型 和 子类型

类 与 类型

      学习泛型的型变之前,需要先学习本小节的内容,以便更好的理解后面的泛型的型变。在Java中,我们往往会把类型当作相同的概念来使用,但其实它们是两种不同概念。区分类和类型这两种概念的同时,也需要分情况讨论:

  • 非泛型类

      非泛型类的名称可以直接当作类型使用。而在Kotlin中,一个非泛型类至少可以分为两种类型:非空类型可空类型。例如String类,可以分为可空类型String? 和 非空类型String.

  • 泛型类

      而对于泛型类就变得更为复杂了。一个泛型类想得到合法的类型,必须用一个具体的类型作为泛型的类型形参。因此一个泛型类可以衍生出无限数量的类型。例如:Kotlin的List是一个类,不是一个类型。其合法类型:List<String>List<Int>等。

子类 和 子类型

      我们一般将一个类的派生类称为子类,该类称为父类(基类)。例如:IntNumber的派生类,Number作为父类,Int作为子类。

而子类型与子类的定义不一样,子类型的定义:

任何时候期望A类型的值时,可以使用B类型的值,则B就是A的子类型

超类型是子类型的反义词。如果B是A的子类型,那么反过来A就是B的超类型。

  • Int是Number的子类型

      对于非泛型类,其类型会沿袭该类的继承关系,当A是B的父类,同时A类型也是B类型的超类型。当期望A类型的对象时,可以使用B类型的对象进行传递。

  • String是String?的子类型

      所有类的 非空类型 都是该类的 可空类型 的子类型,但反过来不可以。例如:在接收String?类型的地方,可以使用String类型的值来替换。但不能将String?类型的值存储到String类型的值中,因为null不是非空类型变量可以接收的值。(除非进行判空或非空断言,编译器将可空类型转换为非空类型,这时原可空类型的值可以存储到非空类型的变量中)

  • Int不是String的子类型

      作为非泛型类,IntString没有继承关系,两者间不存在子类型或超类型的关系。

为什么存在型变

      我们都知道非泛型类其类型会沿袭该类的继承关系。但对于泛型类,这是行不通的。例如以下代码,是无法编译成功的:

#daqiJava.java
List<String> strList = new ArrayList();
List<Object> objList = new ArrayList();
objList = strList;
复制代码

      List<Object>List<String>是两个相互独立的类型,不存在子类型的关系。即便String类的基类是Object类。

      因为当你期望List<Object>时,允许赋值一个List<String>过来,也就意味着其他的类型(如List<Int>等)也能赋值进来。这就造成了类型不一致的可能性,无法确保类型安全,违背了泛型引入的初衷 —— 确保类型安全

      到这里你或许会想,对于接收泛型类对象的方法,这不就"削了"泛型类的代码通用性(灵活性)的能力?Java提供了有限制的通配符来确保类型安全,允许泛型类构建相应的子类型化关系,提高代码的通用性(灵活性)。与之对应的,便是Kotlin的型变。Kotlin中存在协变逆变两种概念,统称为声明处型变

声明处型变

      Kotlin的声明处型变包含了协变逆变。协变和逆变都是用于规范泛型的类型形参的范围,确保类型安全。

协变

  • 协变主要概念:

保留子类型化关系

具体意思是:当 B 是 A 的子类型,那么List<B>就是List<A>的子类型。协变类保留了泛型的类型形参的子类型化关系。

  • 基本定义 使用out关键字
public fun Out(list: List<out String>) {

}
复制代码

逆变

  • 逆变主要概念:

反转子类型化关系

具体意思是:当 B 是 A 的子类型,那么List<A>就是List<B>的子类型。逆变类反转了泛型的类型形参的子类型化关系。

  • 基本定义 使用in关键字
public fun In(list: MutableList<in String>) {

}
复制代码

图解协变和逆变

      对于协变的定义普遍很容易理解,但对于逆变往往比较费解。所以我决定退一步,借助Java的有限制的通配符进行了解。从官方文档中了解到,协变、逆变和Java的通配符类型参数有以下关系:

  • out A 对应Java的通配符类型参数为:? extends A

      通配符类型参数 ? extends A 表示接受 A 或者 A 的子类型。

  • in A 对应Java的通配符类型参数为:? super A

      通配符类型参数 ? super A 表示接受 A 或者 A 的超类型。

所以,out Numberin Number的"取值范围"可以用一张图概括(暂时只考虑由非泛型类的继承带来的子类型化关系):

      Number类具有IntLong等派生类,同时也拥有Any这个基类。当需要依据Number进行协变时(即<out Number>),泛型的类型形参只能选取Number自身以及其子类(子类型)。当需要依据Number进行逆变时(即<in Number>),泛型的类型形参只能选取Number自身以及其基类(超类型)。

      当某方法中需要List<out Number>类型的参数时,将<out Number>转换为<? extends Number>,表示泛型的类型形参可以为Number自身以及其子类(子类型)。即List<Number>协变的子类型集合有:List<Number>List<Int>等。

      List<Int>List<Number>协变的子类型集合中。意味着当需要List<Number>时,可以使用List<Int>来替换,List<Int>List<Number>的子类型。符合协变的要求: IntNumber 的子类型,以致List<Int>也是List<Number>的子类型。

      而如果协变的是List<Int>,那么将<out Int>转换为<? extends Int>。表示泛型的类型形参可以为Int自身以及其子类(子类型)。即List<Int>协变的子类型集合只有:List<Int>

      List<Number>不在List<Int>协变的子类型集合中。意味着当需要List<Int>时,不可以使用List<Number>来替换,List<Number>不是List<Int>的子类型。

      这种思路对于逆变也是可行的。某方法中需要MutableList<in Number>类型的参数时,将<in Number>转换为<? super Number>,表示泛型的类型形参可以为Number自身以及其基类(超类型)。即MutableList<Number>逆变的子类型集合有:MutableList<Number>MutableList<Any>等。

      MutableList<Int>不在MutableList<Number>逆变的子类型集合中。意味着当需要MutableList<Number>时,不可以使用MutableList<Int>来替换,MutableList<Int>不是MutableList<Number>的子类型。

      而如果逆变的是MutableList<Int>,那么将<in Int>转换为<? super Int>。表示泛型的类型形参可以为Int自身以及其基类(超类型)。即MutableList<Int>逆变的子类型集合有:MutableList<Int>MutableList<Number>MutableList<Any>

      MutableList<Number>MutableList<Int>逆变的子类型集合中。意味着当需要MutableList<Int>时,可以使用MutableList<Number>来替换,MutableList<Number>MutableList<Int>的子类型。符合逆变的要求: IntNumber 的子类型,但MutableList<Number>MutableList<Int>的子类型。

可空类型与非空类型的声明处型变

      众所周知,Kotlin中一个非泛型类有着对应的可空类型和非空类型,而且非空类型是可空类型的子类型。因为当需要可空类型的对象时,可以使用非空类型的对象来替换。

      关于可空类型和非空类型间的协变与逆变,也可以使用刚才的方法进行理解,只是这次不再局限于子类和父类,而是扩展到子类型和超类型

  • 当需要依据类型A进行协变时(即<out A>),泛型的类型形参只能选取A自身以及其子类型。
  • 当需要依据类型A进行逆变时(即<in A>),泛型的类型形参只能选取A自身以及其超类型。

      当某方法中需要List<out Any?>类型的参数时,将<out Any?>转换为<? extends Any?>,表示泛型的类型形参可以为Any?自身以及其子类型。即List<Any?>协变的子类型集合有:List<Any?>List<Any>等。

      而如果逆变的是MutableList<Any?>,那么将<in Any?>转换为<? super Any?>。表示泛型的类型形参可以为Any?自身以及其超类型。即MutableList<Any?>逆变的子类型集合有:MutableList<Any?>

      当你试图将MutableList<Any>做为子类型传递给接收MutableList<in Any?>类型参数的方法时,编译器将报错,编译不通过。因为MutableList<Any?>逆变的子类型集合中没有MutableList<Any>

      当某方法中需要List<out Any>类型的参数时,将<out Any>转换为<? extends Any>,表示泛型的类型形参可以为Any自身以及其子类型。即List<Any>协变的子类型集合有:List<Any>

      而如果逆变的是MutableList<Any>,那么将<in Any>转换为<? super Any>。表示泛型的类型形参可以为Any自身以及其超类型。即MutableList<Any>逆变的子类型集合有:MutableList<Any>MutableList<Any?>

      当你试图将List<Any?>做为子类型传递给接收List<out Any>类型参数的方法时,编译器将报错,编译不通过。因为List<Any>协变的子类型集合中没有List<Any?>

in位置 和 out位置

      到这里或许有个疑问,我该依据什么来选择协变或者逆变呢?这就涉及关键字outin的第二层含义了。

关键字out的两层含义:

  • 子类型化被保留。
  • T 只能用在out位置。

关键in的两层含义:

  • 子类型化被反转。
  • T 只能用在in位置。

       out位置是指:该函数生产类型为T的值,泛型T只能作为函数的返回值。而in位置是指:该函数消费类型T的值,泛型T作为函数的形参类型。

消费者 和 生产者

      Kotlin的型变遵从《Effective Java》中的 PECS (Producer-Extends, Consumer-Super)。只能读取的对象作为生产者,只能写入的对象作为消费者。

  • out关键字使得一个类型参数协变:只可以被生产而不可以被消费。

      out修饰符确保类型参数 T 从 Iterator<T> 成员中返回(生产),并从不被消费。

public interface Iterator<out T> {
    public operator fun next(): T
    public operator fun hasNext(): Boolean
}
复制代码
  • in关键字使得一个类型参数逆变:只可以被消费而不可以被生产。

      in 修饰符确保类型参数 TComparable<T> 成员中写入(消费),并从不被生产。

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}
复制代码

out位置

      配合协变分析,可以清楚out为什么扮演生产者角色:

  • 1、由于协变的关系,List<Int>List<Long>等子类型可以替代List<Number>,传递给接收List<Number>类型的方法。而对外仍是List<Number>,但并不知道该泛型类实际的类型形参是什么。
  • 2、当对其进行写入操作时,可以接收Number的任何子类型。但由于不知道该泛型类实际的类型形参是什么。对其进行写入会造成类型不安全。(例如:可能接收的是一个List<Int>,如果你对其写入一个Long,这时就会造成类型不安全。)
  • 3、当对其进行读取操作时,不管它原本接收的是什么类型形参的泛型实例(不管是List<Int>,还是List<Long>等),返回(生产)的是Number实例。以超类型的形式返回子类型实例,类型安全。

in位置

      配合逆变分析,也可以清楚in为什么扮演消费者角色:

  • 1、由于逆变的关系,Consumer<Number>Consumer<Any>等子类型可以替代Consumer<Number>,传递给接收Consumer<Number>类型的方法。
  • 2、当对其进行写入操作时,可以接收Number的任何子类型。不管接收的是Number的什么子类型,对外始终是Consumer<Number>Consumer<Int>Consumer<Long>等不能传递进来)。以超类型的形式消费子类型实例,类型安全。
  • 3、当对其进行读取操作时,由于不知道该泛型类实际的类型形参是什么(是Number呢,还是Any呢?)。只有使用Any返回(生产)才能确保类型安全,所以读取受限。(也就是说在逆变中,泛型 TNumber时,你返回的不是Number,而是Any。)

UnSafeVariance注解

      那是否意味着out关键字修饰的泛型参数是不是不能出现在in位置 ?当然不是,只要函数内部能保证不会对泛型参数存在写操作的行为,可以使用UnSafeVariance注解使编译器停止警告,就可以将其放在in位置。out关键字修饰的泛型参数也是同理。

      例如Kotlin的Listcontains函数等,就是应用UnSafeVariance注解使泛型参数存在于in位置,其内部没有写操作。

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
复制代码

其他

      构造方法的参数既不在in位置也不在out位置。同时该位置规则只对对外公开的API有效。(即对private修饰的函数无效)

声明处型变总结

协变 逆变 不变
结构 Producer<out T> Consumer<in T> MutableList<T>
Java实现 Producer<? extends T> Consumer<? super T> MutableList<T>
子类型化关系 保留子类型化关系 逆转子类型化关系 无子类型化关系
位置 out位置 in位置 in位置和out位置
角色 生产者 消费者 生产者和消费者
表现 只读 只写,读取受限 即可读也可写

选择逆变、协变和不变

那么使用泛型时,逆变、协变和不变如何选择呢?

  • 首先需要考虑泛型形参的位置:只读操作(协变或不变)、只写读操作(逆变或不变)、又读又写操作(不变)。

      Array中存在又读又写的操作,如果为其指定协变或逆变,都会造成类型不安全:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}
复制代码
  • 最后判断是否需要子类型化关系,子类型化关系主要用于提高API的灵活度。

      如果需要子类型化关系,则只读操作(协变或不变)选择协变,否则不变;只写读操作(逆变或不变),选择逆变,否则不变。

星点投射

      Kotlin的型变分为 声明处型变星点投射。所谓的星点投射就是使用 * 代替类型参数。表示你不知道关于泛型实参的任何信息,但仍然希望以安全的方式使用它。

Kotlin 为此提供了以下星点投射的语法:

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

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

  • 对于 Foo <out T>,其中 T 是一个协变类型参数,Foo <*> 等价于 Foo <out Any?>。 因为 T 未知时,只有读取 Any? 类型的元素是安全的。

  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 因为 T 未知时,没有什么可以以安全的方式写入 Foo <*>

  • 对于普通的 Foo <T>,这其中没有任何泛型实参的信息。Foo<*>读取值时等价于 Foo<out Any?>,因为读取 Any? 类型的元素是安全的;Foo<*>写入值是等价于Foo<in Nothing>

      如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影(以interface Function <in T, out U>为例):

  • Function<*, String> 表示 Function<in Nothing, String>。
  • Function<Int, *> 表示 Function<Int, out Any?>。
  • Function<*, *> 表示 Function<in Nothing, out Any?>。

MutableList<*>和MutableList<Any?>的区别

      可以向MutableList<Any?>中添加任何数据,但MutableList<*>只是通配某种类型,因为不知道其具体什么类型,所以不允许向该列表中添加元素,否则会造成类型不安全。

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射

关注下面的标签,发现更多相似文章
评论