在实际开发中,协变(? extends T
) 和 逆变(? super T
) 常常出现在处理数据流的场景中。我们通过一个简单的例子——设计一个通用的数据处理流水线,来深入理解它们的使用。
需求描述
假设我们在做一个电商项目,需要处理不同类型的订单数据:
- 数据源会提供各种类型的订单(如普通订单、会员订单、促销订单等)。
- 我们需要把这些订单传递到处理器进行处理。
- 不同的处理器可能对父类或子类的数据有不同的操作需求。
基本类型定义
我们有一个基本的订单类和几个子类:
// 父类:订单
class Order {
private String id;
public Order(String id) { this.id = id; }
public String getId() { return id; }
}
// 子类:普通订单
class RegularOrder extends Order {
public RegularOrder(String id) { super(id); }
}
// 子类:会员订单
class MemberOrder extends Order {
public MemberOrder(String id) { super(id); }
}
设计目标:通用数据处理流水线
我们要设计两个通用的方法:
- 读取订单数据的工具(协变场景)。
- 将订单数据传递给处理器(逆变场景)。
1. 协变案例:只读订单数据
**场景:**我们需要一个工具方法,可以接受任意类型的订单列表(普通订单、会员订单等),并读取这些订单信息,但不对列表内容做修改。
代码实现:
// 工具方法:读取订单列表
public static void readOrders(List<? extends Order> orders) {
for (Order order : orders) {
System.out.println("订单ID:" + order.getId());
}
}
协变的意义:
-
? extends Order
表示“接受Order
及其子类列表”。 - 我们只读取列表里的数据,不向列表中添加任何内容。
调用示例:
List<RegularOrder> regularOrders = List.of(new RegularOrder("R001"), new RegularOrder("R002"));
List<MemberOrder> memberOrders = List.of(new MemberOrder("M001"), new MemberOrder("M002"));
// 读取普通订单
readOrders(regularOrders);
// 读取会员订单
readOrders(memberOrders);
协变的关键点:
- 允许传入子类型列表(如
List<RegularOrder>
)。 -
只能“读”,不能“写”。
- 如果试图往列表中添加元素,编译器会阻止。
orders.add(new RegularOrder("R003")); // 编译报错:不能添加数据!
2. 逆变案例:处理订单数据
**场景:**我们有一个订单处理器,需要把普通订单传递进去进行处理操作。由于不同的处理器可能接收更通用的 Order
类型,我们希望代码更灵活。
代码实现:
// 工具方法:添加订单到处理器
public static void processOrders(List<? super RegularOrder> orders) {
orders.add(new RegularOrder("R003")); // 安全添加普通订单
System.out.println("订单已处理!");
}
逆变的意义:
-
? super RegularOrder
表示“接受RegularOrder
及其父类的列表”。 - 我们可以向列表中添加
RegularOrder
或其子类的实例。
调用示例:
List<Order> allOrders = new ArrayList<>(); // 容器可以是父类类型
processOrders(allOrders);
逆变的关键点:
- 允许操作父类列表(如
List<Order>
)。 - 只能“写”,不能安全地“读”。
为什么不能读?
读取的数据类型不确定,只能当作 Object
处理:
Object obj = orders.get(0); // OK:只能当作 Object
3. 协变和逆变结合的设计
我们设计一个通用的数据处理流水线,既能从数据源读取订单,又能将订单传递给处理器。
完整实现:
public static <T> void processPipeline(
List<? extends T> source, // 协变:只读数据源
List<? super T> target // 逆变:只写目标
) {
for (T item : source) {
System.out.println("读取订单:" + ((Order) item).getId());
target.add(item); // 将订单传递到目标列表
}
}
调用示例:
List<RegularOrder> regularOrders = List.of(new RegularOrder("R001"), new RegularOrder("R002"));
List<Order> allOrders = new ArrayList<>();
processPipeline(regularOrders, allOrders);
System.out.println("处理后的订单数:" + allOrders.size());
结果输出:
读取订单:R001
读取订单:R002
处理后的订单数:2
总结:协变与逆变的最佳场景
-
协变(
? extends T
)- 适合只读操作,数据来源多样化。
- 典型场景:数据采集、读取列表内容。
-
逆变(
? super T
)- 适合只写操作,数据目标灵活多样。
- 典型场景:数据存储、操作父类容器。
-
协变与逆变结合
- 适合需要“读取 + 写入”分工明确的场景,如数据流传输、数据转换。
用协变和逆变设计代码,不仅让代码更灵活安全,还能显著提升复用性和可维护性!
推荐阅读文章
- 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
- 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
- HTTP、HTTPS、Cookie 和 Session 之间的关系
- 什么是 Cookie?简单介绍与使用方法
- 什么是 Session?如何应用?
- 使用 Spring 框架构建 MVC 应用程序:初学者教程
- 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
- 如何理解应用 Java 多线程与并发编程?
- 把握Java泛型的艺术:协变、逆变与不可变性一网打尽
- Java Spring 中常用的 @PostConstruct 注解使用总结
- 如何理解线程安全这个概念?
- 理解 Java 桥接方法
- Spring 整合嵌入式 Tomcat 容器
- Tomcat 如何加载 SpringMVC 组件
- “在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
- “避免序列化灾难:掌握实现 Serializable 的真相!(二)”
- 如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
- 解密 Redis:如何通过 IO 多路复用征服高并发挑战!
- 线程 vs 虚拟线程:深入理解及区别
- 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
- 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
- “打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
- Java 中消除 If-else 技巧总结
- 线程池的核心参数配置(仅供参考)
- 【人工智能】聊聊Transformer,深度学习的一股清流(13)
- Java 枚举的几个常用技巧,你可以试着用用