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

2023-10-13 23:20
文章标签 java 学习 kotlin 泛型 类比

本文主要是介绍类比学习——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 这样就会出现类型转换异常 ClassCastExceptionDog 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>() {@Overridepublic void accept(Animal data) {System.out.println(data);}});// 0️⃣   list.forEach(new Consumer<Object>() {@Overridepublic 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? = nullfun 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{@Overridepublic 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 语言中文站

这篇关于类比学习——java 泛型 kotlin 泛型中的 in out where的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/206566

相关文章

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

java中新生代和老生代的关系说明

《java中新生代和老生代的关系说明》:本文主要介绍java中新生代和老生代的关系说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、内存区域划分新生代老年代二、对象生命周期与晋升流程三、新生代与老年代的协作机制1. 跨代引用处理2. 动态年龄判定3. 空间分

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一