Kotlin笔记12-Java和Kotlin中的范型对比(一)

Kotlin中的范型和Java中的比较相似,可以参考我的关于介绍Java范型的文章 :

Java范型那些事(一)

Java范型那些事(二)

Java范型那些事(三)

Java范型那些事(四)

在上述博文中,讲述了为什么Java要在1.5版本中引入范型,以及一些有关Java范型的基本知识点。


如果把一个对象分为声明、使用两部分的话。泛型主要是侧重于类型的声明的代码复用,通配符则侧重于使用上的代码复用。泛型用于定义内部数据类型的参数化,通配符则用于定义使用的对象类型的参数化。

使用泛型、通配符提高了代码的复用性。同时对象的类型得到了类型安全的检查,减少了类型转换过程中的错误。


 范型和数组的型变

 Java中数组是协变的

下面的代码是可以正确编译运行的:

        Integer[] ints = new Integer[3];
        ints[0] = 0;
        ints[1] = 1;
        ints[2] = 2;
        Number[] numbers = new Number[3];
        numbers = ints;
        for (Number n : numbers) {
            System.out.println(n);
        }

在Java中,因为 Integer 是 Number 的子类型,数组类型 Integer[] 也是 Number[] 的子类型,因此在任何需要 Number[] 值的地方都可以提供一个 Integer[] 值。

Java对List泛型不是协变的

也就是说, List 不是 List 的子类型,试图在要求 List 的位置提供 List 是一个类型错误。下面的代码,编译器是会直接报错的:

Kotlin笔记12-Java和Kotlin中的范型对比(一)_第1张图片

就算我们使用通配符,这样写:

Kotlin笔记12-Java和Kotlin中的范型对比(一)_第2张图片

 仍然是报错的。

为什么Number的对象可以由Integer实例化,而ArrayList的对象却不能由ArrayList实例化?list中的声明其元素是Number或Number的派生类,为什么不能add Integer?为了解决这些问题,需要了解Java中的逆变和协变以及泛型中通配符用法。

逆变、协变和不变都是用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)

  •    当A≤B时有f(A)≤f(B)成立,则f(⋅)是协变(covariant)的
  •    当A≤B时有f(B)≤f(A)成立,则f(⋅)是逆变(contravariant)的
  •    当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系,f(⋅)是不变(invariant)的

协变和逆协变都是类型安全的。

 

Kotlin的数组不是协变的

abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Cat(val terrorFactor: Int): Animal(1)

以下数组编译报错:

val dogArr: Array = arrayOf(Dog(1), Dog(2))
val animalArr: Array = dogArr

和Java的普通对象一样,以下代码可以编译通过:

val dog: Dog = Dog(10)
var animal: Animal = dog

而Kotlin中以如下方式定义一个范型类,然后使用时也会编译报错:

class ReadableList{

}

val dogReadable: ReadableList = ReadableList()
 //提示报错,需要ReadableList,但却传了ReadableList
val animalReadable: ReadableList = dogReadable

Kotlin对List泛型是协变的

即以下代码可以编译通过

val dogList: List = listOf(Dog(10), Dog(20))
playAnimal(dogList)

fun playAnimal(animalList: List) {
    ...
}

如何使Java和Kotlin添加协变和逆变支持

Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时就需要使用我们之前讲的通配符? 。

在Java和Kotlin中可以通过一定的方式对默认不支持协变的参数类型添加支持。 但是Java和Kotlin这两种语言处理的方式不同:

  •    Java : use-site variance(使用端型变)
  •    Kotlin : declaration-site variance(声明端型变)

可以看出Java使用的是使用端型变,而Kotlin使用的是声明端型变。这两者有什么区别呢?

个人理解就是使用端型变(use-site variance)就是在具体使用(初始化)某一个Class的对象时进行协变。

Java 通过实现了泛型的协变

List list = new ArrayList<>();  

这里的? extends Number表示的是Number类或其子类,我们简记为C。

这里C <= Number,这个关系成立:List <= List< Number >。即有:

List list1 = new ArrayList();  
List list2 = new ArrayList();  

另外一个例子,具体如下代码所示:

List catList = new ArrayList<>();
List animalList = catList;

可以看到,在我们声明animalList时,对泛型进行了一点修改,使用? extends Animal进行修饰之后,以上代码就可以成功编译并运行了。甚至于我们可以定义一个方法来接受这种参数类型,如下所示:

List cats = new ArrayList<>();
playAnimal(cats);

public static void playAnimal(List animal) {
    ...
}

编译可以顺利通过, 这样我们的代码可扩展性会更高!

⚠️注意:此时除了null之外,不能往animalList中添加任何Animal子类的对象,即以下代码会报错:

Kotlin笔记12-Java和Kotlin中的范型对比(一)_第3张图片

如果可以添加的话,List里面将会持有各种Number子类型的对象(Byte,Integer,Float,Double等等)。Java为了保护其类型一致,禁止向List添加任意对象,不过可以添加null。

Java 通过实现了泛型的逆变

List list = new ArrayList<>();  

? super Number 通配符则表示的类型下界为Number。即这里的父类型F是? super Number, 子类型C是Number。即当F <=C , 有f(C) <= f(F) , 这就是逆变。代码示例:

List list3 = new ArrayList();  
List list4 = new ArrayList();  
list3.add(new Integer(3));  
list4.add(new Integer(4));  
  

也就是说,我们不能往List中添加Number的任意父类对象。但是可以向List添加Number及其子类对象。

 

PECS:何时使用extends?何时使用super?

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。

注意:如果你使用一个生产者对象,如 List,在该对象上不允许调用 add() 或 set()。但这并不意味着该对象是不可变的:例如,没有什么阻止你调用 clear()从列表中删除所有项目,因为 clear() 根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。

 

声明处型变

假设有一个泛型接口 Source,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java
interface Source {
    T nextT();
}

那么,在 Source  类型的变量中存储 Source  实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source strs) {
    Source objects = strs; // !!!在 Java 中不允许
    // ……
} 
  

为了修正这一点,我们必须声明对象的类型为 Source,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:

interface Source {
    fun nextT(): T
}

fun demo(strs: Source) {
    val objects: Source = strs // 这个没问题,因为 T 是一个 out-参数
    // ……
}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C 可以安全地作为 C的超类。

简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者

out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。

另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable

interface Comparable {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable  的变量
    val y: Comparable = x // OK!
}

我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:

存在性(The Existential) 转换:消费者 in, 生产者 out! :-)

 

在定义一个类的时候处理, 具体如下所示:

// 使用out关键字
class ReadableList{

}

val dogReadable: ReadableList = ReadableList()
val animalReadable: ReadableList = dogReadable

以上代码跟之前唯一的不同点就是我们在定义ReadableList是对泛型添加了一点限制 out, 然后就可以将dogReadable顺利的赋值给animalReadable对象. 看到这我们应该就能猜到为什么之前Kotlin API中的List<泛型>是支持协变的。

⚠️注意:但是使用out关键字修饰了之后,在ReadableList类内部不可以有以T为参数类型的方法

 

 


参考: 

  • https://github.com/EasyKotlin/chapter6_generics
  • https://huanglizhuo.gitbooks.io/kotlin-in-chinese/content/ClassesAndObjects/Generics.html
  • 深入理解Java与Kotlin的泛型(Generic Type)和型变(Variance)
  • Kotlin中文网:https://www.kotlincn.net/docs/reference/generics.html

 

你可能感兴趣的:(Kotlin,Java,Kotlin)