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

775 阅读8分钟

在学习 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 语言中文站