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的超类型。
- Int是Number的子类型
对于非泛型类,其类型会沿袭该类的继承关系,当A是B的父类,同时A类型也是B类型的超类型。当期望A类型的对象时,可以使用B类型的对象进行传递。
- String是String?的子类型
所有类的 非空类型 都是该类的 可空类型 的子类型,但反过来不可以。例如:在接收String?
类型的地方,可以使用String
类型的值来替换。但不能将String?
类型的值存储到String
类型的值中,因为null
不是非空类型变量可以接收的值。(除非进行判空或非空断言,编译器将可空类型转换为非空类型,这时原可空类型的值可以存储到非空类型的变量中)
- Int不是String的子类型
作为非泛型类,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>
的子类型。协变类保留了泛型的类型形参的子类型化关系。
- 基本定义 使用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 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中一个非泛型类有着对应的可空类型和非空类型,而且非空类型是可空类型的子类型。因为当需要可空类型的对象时,可以使用非空类型的对象来替换。
关于可空类型和非空类型间的协变与逆变,也可以使用刚才的方法进行理解,只是这次不再局限于子类和父类,而是扩展到子类型和超类型。
- 当需要依据类型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位置
到这里或许有个疑问,我该依据什么来选择协变或者逆变呢?这就涉及关键字out
和in
的第二层含义了。
关键字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
修饰符确保类型参数 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位置
配合逆变分析,也可以清楚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
修饰的函数无效)
声明处型变总结
协变 | 逆变 | 不变 | |
---|---|---|---|
结构 | 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<*>只是通配某种类型,因为不知道其具体什么类型,所以不允许向该列表中添加元素,否则会造成类型不安全。
参考资料:
- 《Kotlin实战》
- Kotlin官网