Kotlin学习笔记(四)泛型

因为Kotlin本质上还是Java, 所以Kotlin的泛型和Java泛型一样都是在编译期擦除类型的。那么Kotlin的泛型做了什么优化呢? 就是针对“协变”,“逆变”,“不变”,这些型变使用做了些许简化。

先复习下Java的型变。

Java泛型的通配符

类型系统有三种基本形态,“协变”,“逆变”,“不变”。如何理解它们要从Java的类型通配符说起。

Java泛型的通配符有两种形式:

  • ?extends T: 子类型上界限定符,指定类型参数的上限。该类型必须是T或者T的子类型。
  • ?super T:超类型下界限定符, 指定类型参数的下限。该类型必须是T或者T的父类型。
  • 以上两者就是类型通配符。
public class Animal {

   public void act(List<? extends Animal> list){
       for(Animal animal in list){
           animal.eat() 
       }
   }
}

public class Cat extends Animal{}

public class Dog extends Animal{}

基于上面的代码,有两种情况:

  • 虽然Cat和Animal,Dog与Animal都是继承关系,但是 List<Cat>和List<Animal>, List<Dog>和List<Animal>却都不是继承关系,不存在父子关系的类型。所以下面的代码都是编译不过的:
    List<Animal> animals = new ArraryList<Animal>();
    List<Dog> dogs = new ArraryList<Dog>();
    List<Cat> cats = new ArraryList<Cat>();
    
    animals = dogs; //没有继承关系,编译不过
    
    animals = cats; //没有继承关系,编译不过
    
  • 对于任何的List<T>, 这里的T只要是Animal的子类型,那么List<? extends Animal>就是List<T>的父类型。所以下面的代码是可以编译通过的:
    List<?extends Animal> animals = new ArraryList<>();
    List<Dog> dogs = new ArraryList<Dog>();
    List<Cat> cats = new ArraryList<Cat>();
    
    animals = dogs; //有继承关系,编译通过
    
    animals = cats; //有继承关系,编译通过
    
  • 我们不能在List<?extends Animal>中添加元素。因为对于set/add方法类型, 编译器无法知道具体类型,所以会拒绝这个调用。

    List<?extends Animal> animals = new ArraryList<>();
    List<?extends Animal> animals1 = new ArraryList<>();
    List<Dog> dogs = new ArraryList<Dog>();
    animals.add(new Dog()); //编译不过
    animals.add(new Cat());//编译不过
    animals.addAll(animals1 )//编译不过
    
    animals = dogs; //编译通过, 下面的方法依旧编译不过
    
    animals.add(new Dog()); //编译不过
    animals.add(new Cat());//编译不过
    animals.addAll(animals1 )//编译不过
  • 如果是get方法类型, 那是允许的。因为编译器知道List<?extends Animal>可以转换成Animal类型。

    List<?extends Animal> animals = new ArraryList<>();
    List<Dog> dogs = new ArraryList<Dog>();
    dogs.add(new Dog());  
    dogs.add(new Dog()); 
    
    animals = dogs;  
    
    Animal animal = new Animal();
    animal.act(animals);
    
    //在animal的act方法中,可以读取List<?extends Animal> animals
       public void act(List<? extends Animal> list){
           for(Animal animal in list){
               animal.eat() 
           }
       }
  • 无界通配符, 就是单独一个?。比如List<?>,?可以代表任意类型, 也就是未知类型。同样,我们只能调用get类型的方法,不能调用add/set类型的方法。 因为add/set类型,编译器始终无法确定一种类型。

泛型和数组的型变

在Java中数组是协变的,所以下面的代码可以编译通过:

Animal[] animals = new Animal[3];
animals [0] = new Animal();
animals [1] = new Animal();
animals [2] = new Animal();

Dog[] dogs = new Dog[3];
dogs [0] = new Dog();
dogs [1] = new Dog();
dogs [2] = new Dog();

for(Dog dog in dogs){
    dog .eat()
}

所以数组Dogs[]就是Animal[]的子类型。数组中可以这样做,你说编译器可以确定对象,可是List中怎么又不能确定对象了呢?也可以确定对象啊。以上所有代码中说到的编译通过或不通过,都需要了解Java中的协变,逆变以及泛型中通配符用法。

  • 三种型变定义
  1. Animal类型(简记为 F, Father就是父类, Dog类型(简记为C,Child就是子类),它们继承关系。我们这种关系表示为: F <| C
  2. List<类型>, 分别记为f(F), f(C)。 我们就可以描述协变,逆变,不变了。
  3. 当F<|C成立时,如果有f(F)<| f(C)成立, 那么List f 叫做协变。
  4. 当F<|C成立时,如果有f(C)<| f(F)成立, 那么List f 叫做逆变。
  5. 如果上面两种关系都不成立,就是叫做不变。
  6. 协变和逆变都是类型安全的。
  7. Java中泛型是不变的

<? entends T >实现了泛型的协变

前面已经讲过,下面的代码都是可以的,所以<? entends T >实现了泛型的协变是实现了协变的。但是不能添加元素(除了null),那是因为Java觉得这样List里面会有各种各样的对象,太过于复杂,为了保护类型一致性,就设置了这个限制条件。

List<?extends Animal> animals0 = new ArraryList<Cat>();
List<?extends Animal> animals1 = new ArraryList<Dog>();

<? super T >实现了泛型的逆变

List<? super Animal> list = new ArrayList<>(), 其中? super Animal通配符表示下界为Animal, 即这里的父类型F是? super Animal, 子类型C是Animal。即当F<|C成立,有f(C)<|f(F), 这就是逆变。

实例代码如下:

List<? super Animal> list1 = new ArrayList<Animal>();
List<? super Animal> list2 = new ArrayList<Object>();

list1.add(new Dog);
list2.add(new Dog);


Object object = list1.get(0); //默认是Object
Dog dog = list1.get(0); //需要强制类型转换

也就是说, 我们不能往List<? super Animal>中添加Animal的任意父类对象。却可以向List<? super Animal>添加Animal的子类对象。好搞脑子的地方。

PECS

那么什么时候用extends, 什么时候有用super呢?根据我们前面分析的结果, 简单的说,extends适合get类型,也就消费使用型。super适合set/add类型,也就是生产生成型。我看一下典型事例:这是java.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());
            }
        }
    }

   在public static <T> void copy(List<? super T> dest, List<? extends T> src)中,dest可以插入,而src被限制了只能读取。这样src就不会被修改了。

Kotlin中的泛型

前面讲了很多java的泛型, 现在终于可以将Kotlin的泛型了。

  • out T和 in T

Kotlin抛弃了Java通配符 ?extends T和?super T, 取而代之的是out T和 in T。也就是说Kotlin采用了PECS。生产者和消费者。

Kotlin把保证读取数据安全的对象叫做生产者, 用out T标记。而把保证数据写入安全的对象叫做消费者, 用in T标记。

  • 声明称型变, 在类或接口上定义安全泛型类型,以实现协变。
interface Source<out T>{
 fun <T> next():T
}

所以下面的代码是合法的:

fun demo(str:Source<String>){
    
    val obj:Source<Any> = str //这是合法的。
}

因为Collection接口和Map接口都实现了Iterable接口, 而Iterable接口被声明为生产者接口, 所以所有的Collection和Map对象都可以实现安全的类型协变。

val c:List<Number> = listOf(1,2,3,4) 

由于Kotlin支持类型推断,listOf产生的就是List<Int>。因为List接口实现了安全的类型协变,所以可以安全地把List<Int>类型赋值给List<Number>类型变量。

  • 类型投影

就是对参数做限定,避免不安全操作。 就是前面讲的对out/in的使用。

Kotlin也有对应的星投影语法*, 类似于Java的?, 不过*投影是安全的。

如果类型被声明为 interface Function<in T , out T>, 我们有以下星投影:

Function<*, String>表示Function<in Nothing, String>

Function<Int, *>表示Function<in Int, out Any?>

Function<*, *>表示Function<in Nothing, out Any?>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值