类比学习——java 泛型& kotlin 泛型中的 in out where

在学习 kotlin 泛型的时候,经常会遇到 in out 这两个词,一会用in 一会用out,为啥这里要用 out ?为啥哪里用 in ?啥什么用 out 啥时候用in ?对应上面问题以前我是晕乎乎的,不是很明白,于是打算写这篇文章梳理一下,搞清楚怎么回事。

Java 泛型

在学kotlin 泛型之前,先回顾一下Java中的泛型
为了方便说明引入下面几个类
在这里插入图片描述


具体代码

public class Animal { }

public class Dog extends Animal{ }

public class Cat extends Animal{ }

public class Corgi extends Dog { }

public class Result<T>{
    private T data;
    
    public Result() { }
    public Result(T data) { this.data = data; }
    
    public void setData(T data) { this.data = data; }

    public T getData() {   return data;  }
}


1.不可型变

Result<Dog> dogResult = new Result<>();
Result< Animal> animalResult = dogResult; // 编译错误

虽然 Dog 是 Animal 的子类,但是Java 泛型是不可以型变的,Result<Dog> 对象不能赋值给 Result<Animal> , 他们之间没有关系。
如果 Java 泛型不是这样设计的就容易造成运行时异常,例如

 Result<Dog> dogResult = new Result<>();
 Result<Animal> animalResult = dogResult; // 编译器错误 ❌
//假设👆上面这句代码可以编译通过
//那么我们就可以调用set方法设置一个Animal对象
 animalResult.setData(new Animal());
//但是我们用dogResult的get方法取的时候,本以为是Dog 但实际是Animal 这样就会出现类型转换异常 ClassCastException
 Dog dog = dogResult.getData();

所以 Java 这样设计是为了安全考虑。
为了安全,这样的限制显然会失去一些API的灵活性。于是 Java 提供有限制通配符 ? extends X? super X<?> 来提升API 的灵活性。

2.型变性通配符 —— ? extends

Result<Dog> dogResult = new Result<>(new Dog());
  //泛型类型可以是Animal 或者Animal的子类
Result<? extends Animal> animalResult = dogResult; // 编译通过
// animalResult=new Result<Object>(new Object());//父类不行,编译错误 ❌

//Result<? extends Animal> 修改data数据,保证了数据的安全性,不会让dogResult.getData()发生数据转换异常
//  animalResult.setData(new Animal());// 编译错误

//Result<? extends Animal> 泛型限制保证 animalResult.getData()一定是Animal对象,
// 所以可以通过animalResult获取数据是没问题的
 Animal animal = animalResult.getData();

<? extends X> 可以表示泛型是 X 也可以是 X 的子类,所以上面代码中 Result<? extends Animal> animalResult = dogResult; 是可以的,但是为了安全考虑,animalResult 只可以读数据,不可用写入数据,防止出现类型异常。

<? extends X> 可以安全的使用读取数据(返回值为 X 的函数),但不能写入数据(参数为 X 的函数)因为参数需要的是X 还是 X 的那个子类不确定,就像上面的例子 animalResult 指向的类型是 Result 所以setData 应该放入一个Dog对象,但是 animalResult 也可以指向一个 Result<Cat> 对象,我们只能确定 animalResult 指向的对象泛型是Animal 或者是它的子类,但无法确认它到底是具体哪一个类型(是 Dog 还是Cat 还是……),所以setData的时候我们无法确认到底放那个对象。为了安全考虑,写入数据在这种通配符的情况是不被允许的。

总结 ? extends 通配符

  1. 可以协变,如上 Cat、Dog、Corgi 都是 Animal 的子类,所以 Result<Cat>Result<Dog>Result<Corgi> 都可以用 Result<? extends Animal> 表示。? extends 通配符 限定了上界,泛型的类型可以是 Animal 或它的子类,但不能超过它,即不能是它的父类。
  2. 此通配符,只可以读不可用写,这种对象通常称为消费者。

3.逆变型通配符 —— ?super

        Result<Dog> dogResult = new Result<>(new Dog());
        Result<Object> objResult = new Result<>(new Object());
        //泛型类型可以是Animal 或者Animal的父类
        Result<? super Animal> animalResult = objResult; // 编译通过
        // 编译错误,Dog是Animal的子类
//        animalResult=dogResult ❌

        //可以写,使用set方法
        animalResult.setData(new Animal());
        //如果读的话,返回值是Object,无法确认具体类型
        Object data = animalResult.getData();

<? super X> 可以表示泛型是 X 也可以是 X 的父类,所以上面代码 Result<? super Animal> animalResult = objResult; 是可以的,但 animalResult=dogResult 不可用。此通配符限制了泛型是 X 也可以是 X 的父类,所以通配符,是可以安全的写入数据的(参数为 X 的函数) 但传入的类型必须是 X 或者他的子类,因为 ? super X 可以保证泛型是X 或它的父类,根据类的多态特性,可以使用子类代替父类。如果读的话,无法确认具体的类型,因为只知道是 X 或 它的父类,但具体那个不知道,所有返回的类型是他们顶层父类 Object。

总结 ? super 通配符

  1. 可以逆变 。? super 通配符 限定了下界,泛型的类型可以是 Animal 或它的父类,但不能低于它,即不能是它的子类。
  2. 此通配符,可以写,但读无法确定类型,这种对象通常称为生产者。

4.使用通配符限定参数

? extends X 作为参数

例如 Java 中集合框架中的 addAll 方法

public interface Collection<E> extends Iterable<E> {
   boolean addAll(Collection<? extends E> items);	
}

-----------------------分割线-------------------------

//假设声明一个ArrayList<Animal> 那么它的元素可以是Animal 或者它的子类 
ArrayList<Animal> list = new ArrayList<>();
// 0️⃣  
list.addAll(new ArrayList<Dog>());
list.addAll(new ArrayList<Cat>());
// 1️⃣
// list.addAll(new ArrayList<Object>()); 编译错误❌

声明的集合类型是 ArrayList 所以此时 addAll 的形参类型相当于是 Collection<? extends Animal> 这样就限定了集合泛型必须是 Animal 或者它的子类,于是就保证了通过addAll 方式添加到 list 集合中的元素一定是 Animal 或者它的子类的对象,保证了数据的正确性。

? super X 作为参数
 public void forEach(Consumer<? super E> action) {
      	……

        for(int i = 0; this.modCount == expectedModCount && i < size; ++i) {
            //? super E 通配符可以调用写入方法(参数有 E 的函数)
            action.accept(elementAt(es, i));
        }
        ……
}
 
 -----------------------分割线-------------------------
    
  ArrayList<Animal> list = new ArrayList<>();
// 0️⃣   
 list.forEach(new Consumer<Animal>() {
            @Override
            public void accept(Animal data) {
                System.out.println(data);
            }
  });

// 0️⃣   
 list.forEach(new Consumer<Object>() {
            @Override
            public void accept(Object data) {
                System.out.println(data);
            }
  });

在上面代码中 forEach 的形参类型是 Consumer<? super Animal> , 所以泛型可以值 Animal 或者它的父类,所以不管是 Consumer<Animal> 还是 Consumer<Object> 都是可以的。

kotlin 泛型

kotlin 泛型和 Java 类似,虽然没有 ? extends 和 ?super 这样的通配符 ,但是有类似功能的修饰符 in out
可以简单理解 ?extends 对应 out,? super 对应 in

1.不可型变

val dogResult = Result<Dog>()
val animalResult: Result<Animal> = dogResult // 编译错误

和 Java 一致

2.型变修饰符 —— out

    val dogResult = Result(Dog())
    //泛型类型可以是Animal 或者Animal的子类
    val animalResult: Result<out Animal> = dogResult // 编译通过
// animalResult=new Result<Object>(new Object())//父类不行,编译错误 ❌

//  animalResult.data=Animal()// 编译错误
    
    val animal = animalResult.data

和 Java ?extends 一致,具有型变行,可读不可写

3.型变修饰符 —— int

    val dogResult = Result(Dog())
    val objResult = Result<Any>(Any())
    
    //泛型类型可以是Animal 或者Animal的父类
    val animalResult: Result<in Animal> = objResult // 编译通过
    // 编译错误,Dog是Animal的子类
   //  animalResult=dogResult ❌
    
    //可以写,使用set方法
    animalResult.data = Animal()
    //如果读的话,返回值是Any?,无法确认具体类型
    val data:Any? = animalResult.data

和 Java ?super 一致,具有可逆变性,可写不可读

Kotlin 泛型之 —— 声明处型变

在Java 泛型中我们列举了使用通配符限定参数的方式,看了 java.util.ArrayList 的Api 是如何利用泛型通配符提升Api灵活性的同时,保证数据安全的。 kotlin 也可以按照 ?extends 替换 out,? super 替换 in 的方式限定参数也是可以达到和Java一样的效果。但是如果你看 kotlin.collections.ArrayList 的addAll 的方法并不是我们想的那样。
java.util.ArrayList 的 addAll 方法

 public boolean addAll(Collection<? extends E> item)

按照上面的分析,我们想的 kotlin.collections.ArrayList 的addAll 大概是这样的

   fun addAll(elements: Collection<out E>): Boolean

但实际源码是这样写的

override fun addAll(elements: Collection<E>): Boolean

参数类型是 Collection<E> 而不是 Collection<out E> 虽然没有 out 但参数仍然可以起到限制作用。

    val list = mutableListOf<Animal>() //public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()
    list.addAll(mutableListOf<Dog>())
//    list.addAll(mutableListOf<Any>()) 编译错误
//    list.addAll(mutableListOf<String>()) 编译错误

查看 Collection 你会发现它在定义的时候加上了out

public interface Collection<out E> : Iterable<E> {
 
    public val size: Int
	……
}

这种在类或接口定义处指定 out ,称之为**声明处型变,**这样声明的接口或类中,只能提供读的方法,不能提供写入的方法,此类型我们打算让他变成具有型变性的。例如我把上面的 Result 改成声明处型变

//原版 java to kotlin
class Result<T>(var data: T? = null)

在这里插入图片描述

我们加上out 发现报错了,因为 out 只读不能写,所以需要吧var 改成 val

在这里插入图片描述

这样我们在使用Result 就是协变的了

 val dogResult:Result<Dog> = Result(Dog())
    //默认就是协变的了,所以 Result<Dog> 对象赋值给 Result<Animal> 类型变量
  val animalResult: Result<Animal> = dogResult // 编译通过
  val animal = animalResult.data

声明类型的时候 out 就可以不用写了,你要写了人家还提示你多余在这里插入图片描述

说完 out,in 也是同理,我们也可以在类或接口上指定

class Result<in T>{
    private var data: T? = null
    
    fun setData(data: T?){ this.data=data }

    编译报错,只写,不能读
   // fun getData(): T? =data
}

这样我们在使用Result 就是逆变性的

    val objResult: Result<Any> = Result()
    val animalResult: Result< Animal> = objResult // 编译通过
    // 编译错误,Dog是Animal的子类
    //  animalResult=Result<Dog>() //❌
    //可以写,使用set方法
    animalResult.setData(Animal())

kotlin 中的where

看kotlin的wherect之前,还是看看Java中类似的语法


泛型函数

//定义  在方法返回类型前用 <T> 声明泛型
public static <T> void test(T data) { }
 -----------------------分割线-------------------------
//使用 也放任意类型
test("");
test(new Animal());

加限制的泛型函数

//定义
public static <T extends Animal>  void test(T data) { }
-----------------------分割线-------------------------
//使用 只可以传入 Animal或它的子类
test(new Dog());
test(new Animal());

加多条限制的泛型函数

 //定义  多个限定用 & 连接 只能有一个类且必须放在首位,可以有多个接口
public static <T extends Animal & Serializable & Closeable> void test(T data) {}

    static class Data extends Animal implements Serializable,Closeable{

        @Override
        public void close() throws IOException { }
    }

    public static void main(String[] args) {
        //接收的泛型必须是继承Animal 实现 Serializable,Closeable接口的类型
        test(new Data());
//      test(Animal());//编译错误
    }

Kotlin 中的 where 就是用来实现 Java 中 多条限制的泛型函数

fun <T> test(data: T) where T : Serializable, T : Animal, T : Closeable {}

 class Data : Animal(), Serializable, Closeable {
    override fun close() {
    }
}

fun main() {
    test(Data())
//  test(Animal())//编译错误
}

参考文档


泛型:in、out、where - Kotlin 语言中文站