Kotlin中的泛型

5,615 阅读12分钟

博客地址:sguotao.top/Kotlin-2018…

一个生产环境问题引发的思考。

在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<String> list = new ArrayList<String> ();
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<V> { //泛型形参,常见的泛型形参标识如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<String> cStr = new CustomGenerics<String>("sguotao");
    CustomGenerics<Integer> cInt = new CustomGenerics<Integer>(9456);
}

总结一下泛型类中的一些注意事项:

  1. 在实例化泛型类时,要指定泛型实参。(即需要指定具体类型)
  2. 指定的泛型实参类型只能是类类型,不能是基本数据类型。(不能是int,long,可以是Integer,Long等)

泛型接口

声明泛型接口的格式与声明泛型类相似:

interface 接口名<泛型,泛型……>{
 ……
}

比如定义如下泛型接口。

interface CustomGenericsInterface<T> {
    public T generate();
}

在实现泛型接口的类中,指定泛型实参。

class CustomImpl implements CustomGenericsInterface<String> {
    public String generate() {
        return "hello";
    }
}

泛型方法

声明泛型方法的格式:

[访问权限] <泛型> 返回值类型 方法名( 泛型 参数名)

比如上面的泛型类中定义如下的泛型方法:

class CustomGenerics<V> { //泛型形参,常见的泛型形参标识如T、E、K、V等
    private V value; //成员变量的类型为V,V是在实例化是外部传入的。

    CustomGenerics(V value) {
        this.value = value;
    }

    public V getValue() {
        return value;
    }

    //泛型方法
    public <T> V genericMethod(T t1, V v1) { //泛型方法需要在返回值类型前有<泛型>的标记
        //泛型方法中可以使用泛型方法声明的泛型,也可以使用泛型类声明的泛型
        return value;
    }

    //泛型方法
    public <V> void genericMethod(V v1) {//泛型方法中声明的泛型参数V与泛型类中声明的泛型T不是同一个
    }
    
    //泛型方法
    public static <T> void genericStaticMethod(T t1) {//静态的泛型方法无法使用泛型类中声明的泛型

    }
}

总结一下泛型方法中的一些注意事项:

  1. 判断一个方法是否为泛型方法最直接的方式,看方法返回值前是否有<泛型>的标记;
  2. 在泛型类中声明的泛型方法,即可以使用泛型类中声明的泛型,也可以使用泛型方法中声明的泛型;比如上面示例中的泛型方法genericMethod(T t1, V v1) 。
  3. 如果泛型方法中声明的泛型参数与泛型类中的泛型参数相同,那么可以认为泛型方法中的泛型参数覆盖了泛型类中的泛型参数,比如上面示例中的genericMethod(V v1)。
  4. 还有一点需要指出,泛型类中的使用了泛型参数,但是在返回值前没有<泛型>标记的方法,不是泛型方法,比如上面示例中的getValue(),该方法只是使用了泛型参数,并不是泛型方法。
  5. 如果泛型方法是静态方法,那么此时泛型方法是无法使用泛型类中声明的泛型,比如上面示例中的泛型方法genericStaticMethod(T t1),该方法只能使用泛型方法中的泛型T,无法使用泛型类中的泛型V。

泛型擦除

Java中的泛型是伪泛型,要了解伪泛型,先来了解什么是真泛型?在C#中使用的泛型,就是真泛型。如在C#中定义泛型:

 //泛型类
public class GenericClass<T>
 {
    T _t;
     public GenericClass(T t)
    {
        _t = t;
    }
    public override string ToString()
    {
        return _t.ToString();
    }
}
    
public class Program
{
    static void Main(string[] args)
    {
        GenericClass<int> gInt = new GenericClass<int>(123456); 
        Console.WriteLine(gInt.GetType());
        Console.WriteLine(gInt.ToString());

        GenericClass<string> gStr = new GenericClass<string>("Test");
        Console.WriteLine(gStr.GetType());
        Console.WriteLine(gStr.ToString());

        Console.Read(); 
     }
}

查看输出结果:

20180903153594145422523.png
查看IL发现:
20180903153594152962628.png
而Java中的泛型只存在于编译期,在生成的字节码文件中是不包含任何泛型信息的。比如下面的两个方法,在字节码中具有相同的函数签名。
20180903153594325756741.png
使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。

在C#里面泛型无论在程序源码中、编译后的IL中或是运行期的CLR中都是切实存在的,List与List就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据。

在Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类型。

通配符?及型变

是否有这样的疑问,为什么Number的对象可以由Integer实例化,而ArrayList的对象却不能由ArrayList实例化?先来看下面的代码:

Number num = new Integer(1);  
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch

Integer是Number的子类,所以Number对象可以由Integer实例化,这是Java多态的特性,那么Integer是Number的子类,List是不是List 的父类呢?答案是否定的。List和List没有继承关系,看似需要声明多个方法来接收这些不同的泛型实参了,这显然与Java的多态性是相背离的。

这时,就需要一个在逻辑上可以用来表示同时是List和List 的父类的一个引用类型,由此,类型通配符应运而生。Java中的通配符用?来表示,通配符?可以认为是任意类型的父类,它是一个具体的类型,是泛型实参,这里需要注意与泛型形参T、V的区别。

通配符的引入不只是解决了泛型实参之间的逻辑关系,更重要的一点,对泛型引入了边界的概念。

通配符?的上界

通配符的上界使用<? extends T>的格式,表示类或者方法接收T或者T的子类型,比如:

List<? extends Number> list = new ArrayList<Number>();

通配符?的上界,又可以称为协变。

通配符?的下界

通配符的下界使用<? super T>的格式,表示类或者方法接收T或者T的父类型,比如:

 List<? super Integer> list = new ArrayList<Number>();

通配符?的下界,又称为逆变。关于逆变和协变,下面详细介绍:

协变和逆变

Java中的泛型是既不支持协变,也不支持逆变。那什么是逆变,什么是协变?简单的说,协变就是定义了类型的上边界,而逆变则定义了类型的下边界。看一个协变的例子:

ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
List<? extends Number> list = new ArrayList<Number>();

? extends Number的含义是:接收Number的子类,也包括Number,作为泛型实参。再来看逆变。

在Java中是不能将父类的实例赋值给子类的变量,但是在泛型中可以通过通配符?来模拟逆变,比如:

List<? super Integer> list = new ArrayList<Number>();

? 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 <T> void copy(List<? super T> dest, List<? extends T> 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<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

copy方法实现从源src到目的dest的复制,源src可以看作是生产者,使用协变,目的dest可以看作是消费者,使用逆变。

Kotlin中的泛型

前面用了大量的篇幅来介绍Java中的泛型,其实了解了Java中的泛型,就会使用Kotlin中的泛型,区别仅仅是写法和关键字上的区别。

Kotlin中的泛型方法,比如:

class GenericKotlin<T>(var value: T) {//声明泛型类

    //声明泛型方法
    public fun <V> genericMethod(t: T, v: V): Unit {
    }
}

//声明泛型接口
interface GenericKotlinInterface<T> {
    public fun generate(): T
}

Kotlin中的型变

先来看一下Kotlin中的型变:

fun main(args: Array<String>) {
    //协变
    val list: List<Number> = listOf(1, 2, 3, 4)
    //逆变
    val comparable: Comparable<Int> = object : Comparable<Any> {
        override fun compareTo(other: Any): Int {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
    }
}

我们看一下List的声明源码:

public interface List<out E> : Collection<E> {
   ……
    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<in T> {
    /**
     * 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修饰。

  1. 在泛型形参前面加上out关键字,表示泛型的协变,作为返回值,为只读类型,泛型参数的继承关系与类的继承关系保持一致,比如List和List;
  2. 在泛型参数前面加上in表示逆变,表示泛型的逆变,作为函数的参数,为只写类型,泛型参数的继承关系与类的继承关系相反,比如Comparable和Comparable。

星投影

Kotlin中的星投影,用符号来表示,作用类似于Java中的通配符?,比如当不确认泛型类型时,可以使用来代替。需要注意的时,*只能出现在泛型形参的位置,不能作为在泛型实参。

 //星投影
val list: MutableList<*> = ArrayList<Number>()

参考链接

  1. www.jprl.com/Blog/archiv…
  2. Kotlin Bootcamp for Programmers
  3. Kotlin Koans