类比学习——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实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

java图像识别工具类(ImageRecognitionUtils)使用实例详解

《java图像识别工具类(ImageRecognitionUtils)使用实例详解》:本文主要介绍如何在Java中使用OpenCV进行图像识别,包括图像加载、预处理、分类、人脸检测和特征提取等步骤... 目录前言1. 图像识别的背景与作用2. 设计目标3. 项目依赖4. 设计与实现 ImageRecogni

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.