博客地址:https://sguotao.top/Kotlin-2018-11-06-Kotlin中的泛型.html
一个生产环境问题引发的思考。
在JDK1.5之前,生产环境中总是会出现这样类似的问题:
List list = new ArrayList ();
list.add ("foo");
list.add (new Integer (42)); // added by "mistake"
for (Iterator i = list.iterator (); i.hasNext (); ) {
String s = (String) i.next ();
// ClassCastException for Integer -> String
// work on `s'
System.out.println (s);
}
由于add()接受Object类型,可能在开发调试阶段测试数据使用的都是String类型,直到上线到生产环境中,某次传入了一个Integer类型数据,于是系统崩溃了……
为了解决此类的数据类型安全问题,Java在JDK1.5中提出了泛型,于是问题提早的在开发阶段暴露出来:
// constructed generic type
List list = new ArrayList ();
list.add ("foo");
list.add (42); // error: cannot find symbol: method add(int)
for (String s : list)
System.out.println (s);
除了解决数据类型安全问题,泛型的引入也更多的使用到设计模式当中。泛型的本质就是让类型也变成参数。比如定义函数时声明形参,在调用函数时传入实参。类型的参数化也同样,定义函数或类时声明成泛型(泛型形参),在调用或实例化时传入具体的类型(泛型实参)。
Java中的泛型
Java中泛型可以用在类、接口和方法中,分别称为泛型类、泛型接口和泛型方法。下面分别来看一下。
泛型类
泛型应用在类的声明中,称为泛型类,其格式如下:
[访问权限] class 类名 <泛型,泛型……>{
……
}
比如定义如下泛型类:
class CustomGenerics { //泛型形参,常见的泛型形参标识如T、E、K、V等
private V value; //成员变量的类型为V,V是在实例化是外部传入的。
CustomGenerics(V value) {
this.value = value;
}
public V getValue() {
return value;
}
}
类型创建的格式如下:
类名<具体类型> 对象名称 = new 类名<具体类型>()
比如实例化上面定义的泛型类。
public class TestGenerics {
//在实例化泛型类时,指定泛型实参
CustomGenerics cStr = new CustomGenerics("sguotao");
CustomGenerics cInt = new CustomGenerics(9456);
}
总结一下泛型类中的一些注意事项:
- 在实例化泛型类时,要指定泛型实参。(即需要指定具体类型)
- 指定的泛型实参类型只能是类类型,不能是基本数据类型。(不能是int,long,可以是Integer,Long等)
泛型接口
声明泛型接口的格式与声明泛型类相似:
interface 接口名<泛型,泛型……>{
……
}
比如定义如下泛型接口。
interface CustomGenericsInterface {
public T generate();
}
在实现泛型接口的类中,指定泛型实参。
class CustomImpl implements CustomGenericsInterface {
public String generate() {
return "hello";
}
}
泛型方法
声明泛型方法的格式:
[访问权限] <泛型> 返回值类型 方法名( 泛型 参数名)
比如上面的泛型类中定义如下的泛型方法:
class CustomGenerics { //泛型形参,常见的泛型形参标识如T、E、K、V等
private V value; //成员变量的类型为V,V是在实例化是外部传入的。
CustomGenerics(V value) {
this.value = value;
}
public V getValue() {
return value;
}
//泛型方法
public V genericMethod(T t1, V v1) { //泛型方法需要在返回值类型前有<泛型>的标记
//泛型方法中可以使用泛型方法声明的泛型,也可以使用泛型类声明的泛型
return value;
}
//泛型方法
public void genericMethod(V v1) {//泛型方法中声明的泛型参数V与泛型类中声明的泛型T不是同一个
}
//泛型方法
public static void genericStaticMethod(T t1) {//静态的泛型方法无法使用泛型类中声明的泛型
}
}
总结一下泛型方法中的一些注意事项:
- 判断一个方法是否为泛型方法最直接的方式,看方法返回值前是否有<泛型>的标记;
- 在泛型类中声明的泛型方法,即可以使用泛型类中声明的泛型,也可以使用泛型方法中声明的泛型;比如上面示例中的泛型方法genericMethod(T t1, V v1) 。
- 如果泛型方法中声明的泛型参数与泛型类中的泛型参数相同,那么可以认为泛型方法中的泛型参数覆盖了泛型类中的泛型参数,比如上面示例中的genericMethod(V v1)。
- 还有一点需要指出,泛型类中的使用了泛型参数,但是在返回值前没有<泛型>标记的方法,不是泛型方法,比如上面示例中的getValue(),该方法只是使用了泛型参数,并不是泛型方法。
- 如果泛型方法是静态方法,那么此时泛型方法是无法使用泛型类中声明的泛型,比如上面示例中的泛型方法genericStaticMethod(T t1),该方法只能使用泛型方法中的泛型T,无法使用泛型类中的泛型V。
泛型擦除
Java中的泛型是伪泛型,要了解伪泛型,先来了解什么是真泛型?在C#中使用的泛型,就是真泛型。如在C#中定义泛型:
//泛型类
public class GenericClass
{
T _t;
public GenericClass(T t)
{
_t = t;
}
public override string ToString()
{
return _t.ToString();
}
}
public class Program
{
static void Main(string[] args)
{
GenericClass gInt = new GenericClass(123456);
Console.WriteLine(gInt.GetType());
Console.WriteLine(gInt.ToString());
GenericClass gStr = new GenericClass("Test");
Console.WriteLine(gStr.GetType());
Console.WriteLine(gStr.ToString());
Console.Read();
}
}
查看输出结果:
查看IL发现:
而Java中的泛型只存在于编译期,在生成的字节码文件中是不包含任何泛型信息的。比如下面的两个方法,在字节码中具有相同的函数签名。
使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。
在C#里面泛型无论在程序源码中、编译后的IL中或是运行期的CLR中都是切实存在的,List
在Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList
通配符?及型变
是否有这样的疑问,为什么Number的对象可以由Integer实例化,而ArrayList
Number num = new Integer(1);
ArrayList list = new ArrayList(); //type mismatch
Integer是Number的子类,所以Number对象可以由Integer实例化,这是Java多态的特性,那么Integer是Number的子类,List
这时,就需要一个在逻辑上可以用来表示同时是List
通配符的引入不只是解决了泛型实参之间的逻辑关系,更重要的一点,对泛型引入了边界的概念。
通配符?的上界
通配符的上界使用的格式,表示类或者方法接收T或者T的子类型,比如:
List list = new ArrayList();
通配符?的上界,又可以称为协变。
通配符?的下界
通配符的下界使用的格式,表示类或者方法接收T或者T的父类型,比如:
List list = new ArrayList();
通配符?的下界,又称为逆变。关于逆变和协变,下面详细介绍:
协变和逆变
Java中的泛型是既不支持协变,也不支持逆变。那什么是逆变,什么是协变?简单的说,协变就是定义了类型的上边界,而逆变则定义了类型的下边界。看一个协变的例子:
ArrayList list = new ArrayList(); //type mismatch
List list = new ArrayList();
? extends Number的含义是:接收Number的子类,也包括Number,作为泛型实参。再来看逆变。
在Java中是不能将父类的实例赋值给子类的变量,但是在泛型中可以通过通配符?来模拟逆变,比如:
List list = new ArrayList();
? super Integer的含义是:接收Integer的基类,也包括Integer本身作为泛型实参。
协变与逆变的数学定义:
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);
- f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
- f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
- f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
什么时候使用协变和逆变
什么时候使用协变?extends,什么时候使用逆变 ?super,在《Effective Java》中给出了一个PECS原则:
PECS:Producer extends,Customer super
当使用泛型类作为生产者,需要从泛型类中取数据时,使用extends,此时泛型类是协变的;
当使用泛型类作为消费者,需要往泛型类中写数据时,使用suepr,此时泛型类是逆变的。
一个经典的案例就是Collections中的copy方法。
public static void copy(List dest, List src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i di=dest.listIterator();
ListIterator si=src.listIterator();
for (int i=0; i
copy方法实现从源src到目的dest的复制,源src可以看作是生产者,使用协变,目的dest可以看作是消费者,使用逆变。
Kotlin中的泛型
前面用了大量的篇幅来介绍Java中的泛型,其实了解了Java中的泛型,就会使用Kotlin中的泛型,区别仅仅是写法和关键字上的区别。
Kotlin中的泛型方法,比如:
class GenericKotlin(var value: T) {//声明泛型类
//声明泛型方法
public fun genericMethod(t: T, v: V): Unit {
}
}
//声明泛型接口
interface GenericKotlinInterface {
public fun generate(): T
}
Kotlin中的型变
先来看一下Kotlin中的型变:
fun main(args: Array) {
//协变
val list: List = listOf(1, 2, 3, 4)
//逆变
val comparable: Comparable = object : Comparable {
override fun compareTo(other: Any): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
我们看一下List的声明源码:
public interface List : Collection {
……
public operator fun get(index: Int): E
// Search Operations
/**
* Returns the index of the first occurrence of the specified element in the list, or -1 if the specified
* element is not contained in the list.
*/
public fun indexOf(element: @UnsafeVariance E): Int
Kotlin中的协变不再是? extends,而是使用out关键字,语义更加贴切,生产者生产产品使用out。泛型既可以作为函数的参数,也可以作为函数的返回值。当泛型作为函数的返回值时,称为协变点,当泛型作为函数参数时,称为逆变点。
再来看List的源码,这里的List是只读的List,使用out关键字修饰泛型,这里将泛型E作为协变来使用,也就是当做函数的返回值。但是源码中也将E作为函数的参数使用,即当做逆变来使用,由于函数(比如indexOf)并不会修改List,所以加注解@UnsafeVariance来修饰。
再来看Comparable的源码:
public interface Comparable {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
Kotlin中的逆变也不再是? super,而是使用关键字in,消费者消费产品使用in。Comparable中的泛型被声明为逆变,也就说Comparable中泛型T被当做函数的参数。
最后总结一下,泛型既可以作为函数的返回值,也可以作为函数的参数。当作为函数的返回值时,泛型是协变的,使用out修饰;当作为函数的参数时,泛型是逆变的,使用in修饰。
- 在泛型形参前面加上out关键字,表示泛型的协变,作为返回值,为只读类型,泛型参数的继承关系与类的继承关系保持一致,比如List
和List ; - 在泛型参数前面加上in表示逆变,表示泛型的逆变,作为函数的参数,为只写类型,泛型参数的继承关系与类的继承关系相反,比如Comparable
和Comparable 。
星投影
Kotlin中的星投影,用符号来表示,作用类似于Java中的通配符?,比如当不确认泛型类型时,可以使用来代替。需要注意的时,*只能出现在泛型形参的位置,不能作为在泛型实参。
//星投影
val list: MutableList<*> = ArrayList()
参考链接
- http://www.jprl.com/Blog/archive/development/2007/Aug-31.html
- Kotlin Bootcamp for Programmers
- Kotlin Koans