类比学习——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

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入

Springboot项目启动失败提示找不到dao类的解决

《Springboot项目启动失败提示找不到dao类的解决》SpringBoot启动失败,因ProductServiceImpl未正确注入ProductDao,原因:Dao未注册为Bean,解决:在启... 目录错误描述原因解决方法总结***************************APPLICA编

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security

SpringBoot多环境配置数据读取方式

《SpringBoot多环境配置数据读取方式》SpringBoot通过环境隔离机制,支持properties/yaml/yml多格式配置,结合@Value、Environment和@Configura... 目录一、多环境配置的核心思路二、3种配置文件格式详解2.1 properties格式(传统格式)1.

Apache Ignite 与 Spring Boot 集成详细指南

《ApacheIgnite与SpringBoot集成详细指南》ApacheIgnite官方指南详解如何通过SpringBootStarter扩展实现自动配置,支持厚/轻客户端模式,简化Ign... 目录 一、背景:为什么需要这个集成? 二、两种集成方式(对应两种客户端模型) 三、方式一:自动配置 Thick

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与

Java.lang.InterruptedException被中止异常的原因及解决方案

《Java.lang.InterruptedException被中止异常的原因及解决方案》Java.lang.InterruptedException是线程被中断时抛出的异常,用于协作停止执行,常见于... 目录报错问题报错原因解决方法Java.lang.InterruptedException 是 Jav