当前位置: 首页 > news >正文

Java中协变逆变的实现与Kotlin中的区别

一、核心概念回顾:协变与逆变

  • 协变 (Covariance):如果 Cat 是 Animal 的子类型,那么 Producer<Cat> 也是 Producer<Animal> 的子类型。它适用于只读(输出)场景。

  • 逆变 (Contravariance):如果 Cat 是 Animal 的子类型,那么 Consumer<Animal> 是 Consumer<Cat> 的子类型。它适用于只写(输入)场景。

  • 不变 (Invariance)Box<Cat> 和 Box<Animal> 没有关系。它适用于既可读又可写的场景。


二、Java 的实现:使用处变型 (Use-site Variance)

Java 的变型规则通过通配符(Wildcards) 在使用泛型的地方(如方法参数、局部变量声明)来指定。这意味着变型规则是由API的调用者使用者来决定的。

1. 语法与实现
  • 协变<? extends T>

    // 声明一个协变的List,它只能被读取
    List<? extends Number> numbers = new ArrayList<Integer>();
    Number num = numbers.get(0); // OK, 安全地读取为Number
    // numbers.add(100); // 编译错误!不能写入
  • 逆变<? super T>

    // 声明一个逆变的List,它只能被写入
    List<? super Integer> list = new ArrayList<Number>();
    list.add(100); // OK, 安全地写入Integer
    // Integer i = list.get(0); // 编译错误!不能安全读取
    Object obj = list.get(0); // 唯一能读取的方式
2. 特点与影响
  • 规则在使用处指定:每次声明一个泛型变量或参数时,你都需要思考并使用 extends 或 super。这导致了著名的 PECS (Producer-Extends, Consumer-Super) 原则。

  • 灵活性高:同一个泛型类(如 ArrayList)可以在不同的使用场景下被当作协变、逆变或不变的。

  • 语法噪音大:代码中会充斥大量通配符,使得签名变得复杂。

    // 一个复杂的Java方法签名,包含了PECS原则
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {for (int i = 0; i < src.size(); i++) {dest.set(i, src.get(i));}
    }

三、Kotlin 的实现:声明处变型 & 使用处变型 (Declaration-site & Use-site Variance)

Kotlin 同时支持两种方式,但其核心创新和更推荐的是声明处变型。这意味着变型规则是由API的设计者定义泛型类的时候就规定好的。

1. 声明处变型 (Declaration-site Variance) - 核心特性

在定义类或接口时,使用变型修饰符 out 或 in

  • 协变out T

    • 修饰符 out 表示这个泛型类 Producer<T> 中的 T 只用于输出(作为函数的返回类型)。

    • 这相当于向编译器承诺:”这个类绝对不会消费 T 类型的对象,只会生产它们“。

    • 效果Producer<Cat> 自动成为 Producer<Animal> 的子类型,无需任何通配符。

    // 1. 在声明类时,使用 `out` 将其定义为协变
    interface Producer<out T> { // 注意这里的 out 关键字fun produce(): T // T 只出现在 out 位置(返回值)// fun consume(item: T): Unit // 编译错误!T 不能出现在 in 位置(参数)
    }// 2. 使用:无需任何额外语法,直接赋值
    val catProducer: Producer<Cat> = ...
    val animalProducer: Producer<Animal> = catProducer // ✅ OK! 因为 T 是 out的
    val animal: Animal = animalProducer.produce()

    Kotlin 标准库中的 List 接口就是只读的,其泛型参数被声明为 out

    interface List<out E> : Collection<E> { ... } // 因此 List<String> 是 List<Any?> 的子类型
  • 逆变in T

    • 修饰符 in 表示这个泛型类 Consumer<T> 中的 T 只用于输入(作为函数的参数类型)。

    • 这相当于向编译器承诺:”这个类只会消费 T 类型的对象,不会生产它们“。

    • 效果Consumer<Animal> 自动成为 Consumer<Cat> 的子类型。

    // 1. 在声明类时,使用 `in` 将其定义为逆变
    interface Consumer<in T> { // 注意这里的 in 关键字fun consume(item: T): Unit // T 只出现在 in 位置(参数)// fun produce(): T // 编译错误!T 不能出现在 out 位置(返回值)
    }// 2. 使用:无需任何额外语法,直接赋值
    val animalConsumer: Consumer<Animal> = ...
    val catConsumer: Consumer<Cat> = animalConsumer // ✅ OK! 因为 T 是 in的
    catConsumer.consume(Cat()) // 实际调用的是 animalConsumer.consume(Animal)

    Kotlin 标准库中的 Comparable 接口就是逆变的。

    interface Comparable<in T> { // 因此 Comparable<Any> 是 Comparable<String> 的子类型operator fun compareTo(other: T): Int
    }
2. 使用处变型 (Use-site Variance):类型投影 (Type Projection)

Kotlin 也提供了类似 Java 通配符的功能,用于在使用处临时改变型变规则,这被称为类型投影

  • 语法:在具体使用的地方使用 out 或 in

  • 目的:用于处理那些在定义时是不变的泛型类(如 MutableList),但在某个特定函数中,你只想以安全的方式使用它。

// 假设MutableList是不变的(它本来就是,因为既可读又可写)
fun copy(from: MutableList<out Animal>, to: MutableList<in Animal>) {// 这里,我们临时地将 'from' 投影为一个【生产者】// 意味着我们可以从 from 中安全地【读取】Animalfor (animal in from) {// 这里,我们临时地将 'to' 投影为一个【消费者】// 意味着我们可以向 to 中安全地【写入】Animalto.add(animal)}// from.add(Cat()) // 编译错误!'from' 被投影为 out,不能写入// val item: Animal = to[0] // 编译错误!'to' 被投影为 in,不能安全读取
}val cats: MutableList<Cat> = mutableListOf(Cat(), Cat())
val animals: MutableList<Animal> = mutableListOf(Dog())copy(cats, animals) // ✅ OK! 因为使用了类型投影

四、核心区别总结

特性JavaKotlin
核心机制使用处变型 (Use-site)声明处变型 (Declaration-site) 为主,使用处变型(类型投影)为辅
语法关键字? extends T (协变), ? super T (逆变)out T (协变), in T (逆变)
决策者API的调用者/使用者API的设计者(声明处),或调用者(使用处投影)
代码风格PECS原则,通配符大量出现在方法签名中更简洁、更直观。泛型类自身声明其变型性质,使用时常无需额外修饰
List<String> 能否赋值给 List<Object>不能。必须写为 List<? extends Object>可以,但前提是Kotlin的 List 接口已声明为 interface List<out E>
核心思想“我怎么使用你这个不变的盒子?”“我一个什么样的盒子?”

五、常见问题总结

Q:“Java 和 Kotlin 在实现泛型的协变和逆变上有什么主要区别?”

A:

Java 使用的是‘使用处变型’。规则由API的调用者决定。它通过通配符 ? extends T 和 ? super T 在使用泛型的地方(如方法参数)来指定变型规则。这非常灵活,但导致了复杂的方法签名和需要牢记PECS原则。

Kotlin 优先采用‘声明处变型’。规则由API的设计者决定。它在定义泛型类或接口时,使用 out(协变)和 in(逆变)修饰符来规定该类的泛型参数是只用于输出还是只用于输入。这样,在使用时就直接具备协变或逆变的赋值能力,代码非常简洁直观。Kotlin 的 List 是只读的、协变的,就是因为其泛型参数被声明为 out

当然,Kotlin 也提供了类似 Java 的使用处变型,称为类型投影,使用 MutableList<out T> 这样的语法,用于临时处理那些本身是不变的泛型类。

总的来说,Java 的策略是‘使用时再告诉编译器规则’,而 Kotlin 的策略是‘设计时就声明好规则,使用时直接享受其好处’。这使得 Kotlin 代码在泛型方面通常更简洁、更易读。”

http://www.dtcms.com/a/356259.html

相关文章:

  • 如何用 Kotlin 在 Android 手机开发一个应用程序获取国家或地区信息
  • echo、seq、{}、date、bc命令
  • 如何用 Kotlin 在 Android 手机开发一个应用程序获取网络时间
  • OpenCV之霍夫变换
  • 在C++11中实现函数式编程的组合子
  • AI推介-大语言模型LLMs论文速览(arXiv方向):2025.04.25-2025.04.30
  • React Native 初体验
  • rabbitmq学习笔记 ----- 多级消息延迟始终为 20s 问题排查
  • OpenCV 图像预处理核心技术:阈值处理与滤波去噪
  • LubanCat-RK3568 UART串口通信,以及遇到bug笔记
  • CRYPT32!CryptMsgUpdate函数分析和asn.1 editor nt5inf.cat 的总览信息
  • 第八篇 永磁同步电机控制-MTPA、MTPV
  • 深入解析Qt节点编辑器框架:数据流转与扩展机制(三)
  • 实时音视频延迟优化指南:从原理到实践
  • 零知开源——基于STM32F407VET6和ADXL345三轴加速度计的精准运动姿态检测系统
  • Blender模拟结构光3D Scanner(三)获取相机观测点云的真值
  • OpenCV 基础知识总结
  • 无懈可击的 TCP AIMD
  • 亚马逊季节性产品运营策略:从传统到智能化的演进
  • kimi浏览器助手-月之暗面推出的智能浏览器扩展
  • docker中的mysql有中文显示问题跟大小写区分问题?
  • Python从入门到高手9.4节-基于字典树的敏感词识别算法
  • 使用Python脚本执行Git命令
  • React 状态丢失:组件 key 用错引发的渲染异常
  • Rust 安装与运行指南
  • Custom SRP - LOD and Reflections
  • 柳州市委常委、统战部部长,副市长潘展东率队首访深兰科技集团新总部,共探 AI 赋能制造大市与东盟合作新局
  • Claude Code 完整手册:从入门、配置到高级自动化
  • 【python】相机输出图片时保留时间戳数据
  • Linux学习——sqlite3