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

Java中的协变、逆变

在 Java 中,协变(Covariance)逆变(Contravariance) 是泛型系统中的重要概念,用于描述子类型如何在复杂类型(如集合)中传递。

1. 基础概念:什么是协变和逆变?

假设 BA 的子类型(例如 IntegerNumber 的子类型),那么:

  • 协变List<B>List<A> 的子类型(允许从子类型向父类型转换)。
  • 逆变List<A>List<B> 的子类型(允许从父类型向子类型转换)。
  • 不变List<B>List<A> 之间没有继承关系(不允许直接转换)。

Java 中的规则

  • 数组是协变的,但可能导致运行时错误。
  • 泛型是不变的,但可以通过通配符(? extends T? super T)实现有限的协变和逆变。

2. 数组的协变(Java 中的特殊情况)

Java 的数组是协变的,即如果 BA 的子类,则 B[] 可以赋值给 A[]。但这可能导致运行时异常:

Number[] numbers = new Integer[5]; // 合法,数组是协变的
numbers[0] = 3.14; // 运行时错误:ArrayStoreException(数组类型不匹配)

类比
想象一个 “水果篮子”(Fruit[]),可以放入苹果(Apple[])或香蕉(Banana[])。但如果篮子实际装的是苹果(new Apple[5]),你却试图放入香蕉(Banana),就会报错。

3. 泛型的不可协变性(不变性)

Java 的泛型是不可协变的,即 List<B> 不是 List<A> 的子类型,即使 BA 的子类。例如:

List<Number> numbers = new ArrayList<Integer>(); // 编译错误!

为什么泛型不可协变?
如果允许这样的赋值,会破坏类型安全。例如:

List<Integer> ints = new ArrayList<>();
ints.add(10);// 假设允许 List<Integer> 赋值给 List<Number>
List<Number> nums = ints; // 编译错误(实际不允许)
nums.add(3.14); // 如果允许,这里会向 List<Integer> 中添加 Double!
Integer i = ints.get(0); // 运行时 ClassCastException!

类比
想象一个 “苹果篮子”(List<Apple>)和一个 “水果篮子”(List<Fruit>)。虽然苹果是水果的一种,但 “苹果篮子”不是“水果篮子” 的子类。如果允许将 “苹果篮子” 当作 “水果篮子”,就可能向其中放入香蕉,导致类型错误。

4. 泛型通配符实现有限的协变和逆变

虽然泛型本身不可协变,但可以通过通配符实现安全的类型转换:

(1)上限通配符 ? extends T(协变)

允许读取 T 类型的元素,但不能写入(除了 null):

List<? extends Number> numbers = new ArrayList<Integer>(); // 合法
Number num = numbers.get(0); // 合法,读取为 Number
// numbers.add(10); // 编译错误,不能添加任何类型(除了 null)

作用:安全地从集合中读取元素(生产者)。
类比
有一个 “水果篮子”(List<? extends Fruit>),你知道里面装的是水果(或其子类),但不确定具体是哪种水果。因此,你可以安全地从中取出水果(作为 Fruit 类型),但不能向其中添加任何水果(因为不知道篮子的具体类型)。

(2)下限通配符 ? super T(逆变)

允许写入 T 类型的元素,但读取时只能作为 Object

List<? super Integer> numbers = new ArrayList<Number>(); // 合法
numbers.add(10); // 合法,可添加 Integer
Object obj = numbers.get(0); // 读取为 Object

作用:安全地向集合中写入元素(消费者)。
类比
有一个 “篮子”(List<? super Apple>),你知道它可以装苹果(或其父类,如水果、食物)。因此,你可以安全地向其中放入苹果(Apple),但取出时只能当作 “物品”(Object),因为不确定篮子的具体类型。

5. 协变与逆变的对比表

场景数组(协变)泛型(不变)泛型通配符(有限协变 / 逆变)
赋值Number[] = Integer[](合法)List<Number> = List<Integer>(编译错误)List<? extends Number> = List<Integer>(合法)
写入元素可能运行时错误编译错误? extends T:禁止写入 ? super T:允许写入 T
读取元素安全安全? extends T:读取为 T ? super T:读取为 Object
类型安全运行时可能出错编译阶段强制检查编译阶段保证安全

6. 总结:何时使用协变和逆变?

  • 协变(? extends T):当你只需要从集合中读取元素时使用。例如:

    // 安全地读取 List<Integer> 中的元素
    List<? extends Number> nums = List.of(1, 2, 3);
    Number n = nums.get(0); // 合法
    
  • 逆变(? super T):当你只需要向集合中

    // 安全地向 List<Object> 中添加 Integer
    List<? super Integer> nums = new ArrayList<>();
    nums.add(10); // 合法
    

通过通配符,Java 泛型在保证类型安全的前提下,提供了一定的灵活性。理解协变和逆变是掌握 Java 泛型高级用法的关键。

7. 使用场景

(1)只读场景:使用 ? extends T(协变)

如果你只需要从集合中读取元素,使用上限通配符 ? extends T
示例:计算数字集合的总和:

public double sum(List<? extends Number> numbers) {double sum = 0;for (Number num : numbers) {sum += num.doubleValue();}return sum;
}// 可以传入 List<Integer> 或 List<Double>
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // 合法List<Double> doubles = List.of(1.5, 2.5);
sum(doubles); // 合法

限制:不能向 List<? extends Number> 中添加元素(除了 null),因为无法确定具体类型。

(2)只写场景:使用 ? super T(逆变)

如果你只需要向集合中写入元素,使用下限通配符 ? super T
示例:将整数添加到集合中:

public void addNumbers(List<? super Integer> numbers) {for (int i = 0; i < 10; i++) {numbers.add(i); // 合法:可以添加 Integer 类型}
}// 可以传入 List<Integer>、List<Number> 或 List<Object>
List<Number> nums = new ArrayList<>();
addNumbers(nums); // 合法

限制:从 List<? super Integer> 中读取元素时,只能作为 Object 类型,因为无法确定具体类型。

(3)读写兼顾场景:使用不变(无通配符)

如果你需要对集合进行读写操作,则不使用通配符,直接指定具体类型。
示例:操作整数列表:

public void processIntegers(List<Integer> ints) {// 读取元素int first = ints.get(0);// 写入元素ints.add(first * 2);
}// 只能传入 List<Integer>,不能传入 List<Number> 或 List<Object>
List<Integer> ints = new ArrayList<>();
processIntegers(ints); // 合法

限制:类型必须精确匹配,不允许类型转换。

8. 常见误区与注意事项

  1. 数组的协变陷阱
    数组的协变可能导致运行时异常(ArrayStoreException),应尽量避免将子类数组赋值给父类数组变量。

  2. 通配符与子类的区别
    List<? extends Number> 不代表 “所有 Number 子类的列表”,而是 “某个具体子类的列表”。例如:

    List<? extends Number> nums = new ArrayList<Integer>(); // 合法
    // nums.add(3.14); // 编译错误:无法确定具体类型
    
  3. 逆变的读取限制
    List<? super Integer> 中读取的元素只能作为 Object,因为无法确定集合的具体类型。

  4. 泛型方法 vs 通配符
    如果需要在多个参数或返回值之间建立类型关联,优先使用泛型方法而非通配符。例如:

    <T> void swap(List<T> list, int i, int j); // 泛型方法,保证两个索引位置的元素类型一致
    

9. 实战案例

案例 1:生产者场景(读取数据)
// 计算集合中所有数字的平均值
public double average(List<? extends Number> numbers) {double sum = 0;for (Number num : numbers) {sum += num.doubleValue();}return sum / numbers.size();
}
案例 2:消费者场景(写入数据)
// 将元素添加到集合
public void fill(List<? super String> list, String value, int count) {for (int i = 0; i < count; i++) {list.add(value); // 合法:可以添加 String}
}
案例 3:读写兼顾场景(精确类型)
// 对列表中的每个元素执行操作
public void transform(List<Integer> list, Function<Integer, Integer> mapper) {for (int i = 0; i < list.size(); i++) {list.set(i, mapper.apply(list.get(i))); // 读写操作,必须精确类型}
}
http://www.dtcms.com/a/303344.html

相关文章:

  • 【AI绘画】Stable Diffusion webUI 与 ComfyUI 全解析:安装、模型、插件及功能对比
  • 使用宝塔“PostgreSQL管理器”安装的PostgreSQL,如何设置远程连接?
  • 开发避坑短篇(7):Vue+window.print()打印实践
  • Linux中配置haproxy
  • Java 笔记 serialVersionUID
  • 50etf的实值期权和虚值期权谁涨得快?
  • gdb调试教程
  • 图像轮廓与凸包
  • 网络编程接口htonl学习
  • 如何进行DAP-seq的数据挖掘,筛选验证位点
  • Baumer工业相机堡盟工业相机如何通过YoloV8深度学习模型实现面部口罩的检测识别(C#代码,UI界面版)
  • C++-关于协程的一些思考
  • json取值,如果字段不存在,匹配下一个字段
  • 自定义View学习记录 plinko游戏View
  • 恒坤新材IPO被暂缓审议:收入确认法遭质疑,募资缩水约2亿元
  • 元宇宙经济与数字经济的异同:虚实交织下的经济范式对比
  • 基于Springboot的宠物救助管理系统的设计与实现
  • 【VUE3】搭建项目准备工作
  • 艾格文服装软件怎么用?
  • Windows中查看GPU和Cuda信息的DOS命令总结
  • AI产品经理手册(Ch1-2)AI Product Manager‘s Handbook学习笔记
  • uvm sequence Arbitration
  • AI 驱动、设施扩展、验证器强化、上线 EVM 测试网,Injective 近期动态全更新!
  • git stash apply 冲突合并方法解决
  • 希尔排序(缩小增量排序)面试专题解析
  • unisS5800XP-G交换机配置命令之登录篇
  • 洛谷 P10448 组合型枚举-普及-
  • Visual Studio Code使用
  • 25世界职业院校技能大赛国内赛区承办名单确定,各赛区需全力筹备
  • 【Spring Boot 快速入门】二、请求与响应