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);//编译器不通过

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

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

类型参数约束

上界

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

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

image

      换句话说就是,某泛型函数(例如求和函数)可以用在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的超类型。

image
  • Int是Number的子类型

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

  • String是String?的子类型

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

  • Int不是String的子类型

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

image

为什么存在型变

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

#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的"取值范围"可以用一张图概括(暂时只考虑由非泛型类的继承带来的子类型化关系):

image

      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>等。

image

      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>

image

      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>等。

image

      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>

image

      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?>

image

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

image

      当某方法中需要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?>

image

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

image

in位置 和 out位置

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

关键字out的两层含义:

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

关键in的两层含义:

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

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

image

消费者 和 生产者

      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实例。以超类型的形式返回子类型实例,类型安全。
image

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。)
image

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知识归纳(十四) —— 反射

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342