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>
等。
子类 和 子类型
我们一般将一个类的派生类称为子类,该类称为父类(基类)。例如:
Int
是
Number
的派生类,
Number
作为父类,
Int
作为子类。
而子类型与子类的定义不一样,子类型的定义:
任何时候期望A类型的值时,可以使用B类型的值,则B就是A的子类型
超类型 是子类型的反义词。如果B是A的子类型,那么反过来A就是B的超类型。
对于 非泛型类 ,其类型会沿袭该类的继承关系,当A是B的父类,同时A类型也是B类型的超类型。当期望A类型的对象时,可以使用B类型的对象进行传递。
所有类的
非空类型
都是该类的
可空类型
的子类型,但反过来不可以。例如:在接收
String?
类型的地方,可以使用
String
类型的值来替换。但不能将
String?
类型的值存储到
String
类型的值中,因为
null
不是非空类型变量可以接收的值。(除非进行判空或非空断言,编译器将可空类型转换为非空类型,这时原可空类型的值可以存储到非空类型的变量中)
作为非泛型类,
Int
和
String
没有继承关系,两者间不存在子类型或超类型的关系。
为什么存在型变
我们都知道 非泛型类 其类型会沿袭该类的继承关系。但对于 泛型类 ,这是行不通的。例如以下代码,是无法编译成功的:
#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>
的子类型。协变类保留了泛型的类型形参的子类型化关系。
public fun Out(list: List<out String>) {
复制代码
逆变
反转子类型化关系
具体意思是:当 B 是 A 的子类型,那么
List<A>
就是
List<B>
的子类型。逆变类反转了泛型的类型形参的子类型化关系。
public fun In(list: MutableList<in String>) {
复制代码
图解协变和逆变
对于协变的定义普遍很容易理解,但对于逆变往往比较费解。所以我决定退一步,借助Java的有限制的通配符进行了解。从 官方文档 中了解到,协变、逆变和Java的通配符类型参数有以下关系:
通配符类型参数 ? extends A 表示接受 A 或者 A 的子类型。
通配符类型参数 ? super A 表示接受 A 或者 A 的超类型。
所以,
out Number
和
in Number
的"取值范围"可以用一张图概括(暂时只考虑由非泛型类的继承带来的子类型化关系):
Number
类具有
Int
、
Long
等派生类,同时也拥有
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>
的子类型。符合协变的要求:
Int
是
Number
的子类型,以致
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>
的子类型。符合逆变的要求:
Int
是
Number
的子类型,但
MutableList<Number>
是
MutableList<Int>
的子类型。
可空类型与非空类型的声明处型变
众所周知,Kotlin中一个非泛型类有着对应的可空类型和非空类型,而且 非空类型是可空类型的子类型 。因为当需要可空类型的对象时,可以使用非空类型的对象来替换。
关于可空类型和非空类型间的协变与逆变,也可以使用刚才的方法进行理解,只是这次 不再局限于子类和父类,而是扩展到子类型和超类型 。
<out 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位置
到这里或许有个疑问,我该依据什么来选择协变或者逆变呢?这就涉及关键字
out
和
in
的第二层含义了。
关键字out的两层含义:
关键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
修饰符确保类型参数 T
从 Comparable<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
为什么扮演消费者角色:
1、由于逆变的关系,Consumer<Number>
、Consumer<Any>
等子类型可以替代Consumer<Number>
,传递给接收Consumer<Number>
类型的方法。
2、当对其进行写入操作时,可以接收Number的任何子类型。不管接收的是Number的什么子类型,对外始终是Consumer<Number>
(Consumer<Int>
、Consumer<Long>
等不能传递进来)。以超类型的形式消费子类型实例,类型安全。
3、当对其进行读取操作时,由于不知道该泛型类实际的类型形参是什么(是Number
呢,还是Any
呢?)。只有使用Any
返回(生产)才能确保类型安全,所以读取受限。(也就是说在逆变中,泛型 T
为Number
时,你返回的不是Number
,而是Any
。)
UnSafeVariance注解
那是否意味着out
关键字修饰的泛型参数是不是不能出现在in
位置 ?当然不是,只要函数内部能保证不会对泛型参数存在写操作的行为,可以使用UnSafeVariance
注解使编译器停止警告,就可以将其放在in
位置。out
关键字修饰的泛型参数也是同理。
例如Kotlin的List
中contains
函数等,就是应用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
修饰的函数无效)
声明处型变总结
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<*>只是通配某种类型,因为不知道其具体什么类型,所以不允许向该列表中添加元素,否则会造成类型不安全。
参考资料:
《Kotlin实战》
Kotlin官网
android Kotlin系列: