Java通配符深入理解

1. 引入通配符的动机

泛型虽然提供了类型安全,但在某些场景下显得过于严格,尤其是在处理继承关系时。

问题: 假设 IntegerNumber 的子类。那么 List<Integer>List<Number> 的子类吗? 答案是否定的!

泛型类型之间默认是不存在继承关系的,即使它们的类型参数之间有继承关系。这意味着你不能将一个 List<Integer> 赋值给一个 List<Number> 变量,也不能将 List<Integer> 作为参数传递给需要 List<Number> 的方法。

ini 复制代码
List<Integer> integers = new ArrayList<>();
// List<Number> numbers = integers; // 编译错误! Incompatible types.

这样做是为了防止类型安全问题。如果允许 List<Number> numbers = integers;,那么就可以通过 numbers.add(Double.valueOf(3.14));integers 列表里添加 Double 类型,这显然破坏了 integers 列表只能包含 Integer 的约定。

但是,我们确实有需要处理"某一种 Number 的子类"的列表,或者"某一种 Integer 的父类"的列表的需求。这时就需要通配符

2.通配符的概念

通配符 ? 用于表示未知的类型 。它使得泛型能够处理更灵活的子类型关系。通配符主要用在方法参数、字段类型或局部变量类型上,不能用于定义泛型类或泛型方法的类型参数(即 class MyClass<?> 是非法的)。

上界通配符:? extends Type
scss 复制代码
public static double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) { // 可以安全地读取 Number
        sum += n.doubleValue();
    }
    // list.add(Integer.valueOf(1)); // 编译错误! 不能添加
    return sum;
}
​
// 使用
List<Integer> li = List.of(1, 2, 3);
List<Double> ld = List.of(1.1, 2.2, 3.3);
System.out.println("Sum of integers = " + sumOfList(li));
System.out.println("Sum of doubles = " + sumOfList(ld));
下界通配符:? super Type
scss 复制代码
public static void addNumbers(List<? super Integer> list) {
    list.add(1); // 可以安全地添加 Integer
    list.add(2);
    // Number n = list.get(0); // 编译错误! 读取出来的只能保证是 Object
    Object obj = list.get(0); // 可以读取 Object
    System.out.println("Added numbers to list. First element as Object: " + obj);
}
​
// 使用
List<Number> ln = new ArrayList<>();
List<Object> lo = new ArrayList<>();
List<Integer> li = new ArrayList<>();
​
addNumbers(ln); // Number 是 Integer 的父类
addNumbers(lo); // Object 是 Integer 的父类
addNumbers(li); // Integer 本身
System.out.println("List<Number>: " + ln);
System.out.println("List<Object>: " + lo);
System.out.println("List<Integer>: " + li);
无界通配符:?
ini 复制代码
public static void printListInfo(List<?> list) {
    System.out.println("List size: " + list.size());
    // list.add("anything"); // 编译错误!
    if (!list.isEmpty()) {
        Object first = list.get(0); // 可以读取 Object
        System.out.println("First element (as Object): " + first);
    }
}
​
// 使用
List<String> ls = List.of("A", "B");
List<Integer> li = List.of(10, 20);
printListInfo(ls);
printListInfo(li);

3.泛型与通配符的关系与区别

  • 泛型(如 <T> 是用来定义可以接受类型参数的类、接口或方法。它声明了一个类型变量。
  • 通配符(如 ?, ? extends T, ? super T 是用来使用泛型类型时,表示对类型参数的某种约束(未知类型、类型的上界或下界)。它本身不是类型变量,而是表示某种范围内的类型。

4.借助经典的父子类赋值关系(Father f = new Children())类比理解

将Java通配符的核心机制与 Father f = new Children() 这一经典的父子类赋值关系联系起来或许更易于理解。

对于 ? extends Type (比如 ? extends Father),它意味着容器里实际装的是 Father 或者像 Children 这样的子类对象。因此,当你从这个容器中读取 数据时,你得到的对象至少是一个 Father,这就像 Father f = new Children() 这个赋值操作一样,是安全的向上转型,你可以放心地将读取到的对象当作 Father 来使用。

而对于 ? super Type (比如 ? super Children),容器里实际装的是 Children 或者像 Father 这样的父类对象。当你尝试读取 并希望得到 Children 时,这是不安全的,因为你拿到的可能是一个 Father 对象,这就像你不能直接将一个 Father 类型的引用 f 当作 Children 类型来用(需要不安全的向下转型)。然而,当你写入 一个 Children 对象时,却是安全的。为什么呢?因为无论这个容器实际是 List<Children>List<Father> 还是 List<Object>,根据 父类引用 = 子类对象 的原则,它们都能合法地接收一个 Children 对象。所以,这个写入操作的安全性,同样源于那个基础的赋值规则在所有可能父类型上的普遍适用性。

相关推荐
向哆哆1 分钟前
Eclipse Java 开发调优:如何让 Eclipse 运行更快?
java·ide·eclipse
爱晒太阳的小老鼠7 分钟前
策略模式-枚举实现
java·策略模式
77tian14 分钟前
设计模式的原理及深入解析
java·开发语言·单例模式·设计模式·代理模式·享元模式·原型模式
会飞的架狗师1 小时前
【Spring Cloud Gateway】Nacos整合遇坑记:503 Service Unavailable
java·开发语言
重生之后端学习2 小时前
02-前端Web开发(JS+Vue+Ajax)
java·开发语言·前端·javascript·vue.js
字节源流8 小时前
关于maven的依赖下不下来的问题
java·maven
pjx9878 小时前
服务间的“握手”:OpenFeign声明式调用与客户端负载均衡
java·运维·spring·负载均衡
prinrf('千寻)9 小时前
MyBatis-Plus 的 updateById 方法不更新 null 值属性的问题
java·开发语言·mybatis
老华带你飞9 小时前
实习记录小程序|基于SSM+Vue的实习记录小程序设计与实现(源码+数据库+文档)
java·数据库·spring boot·小程序·论文·毕设·实习记录小程序
在未来等你9 小时前
互联网大厂Java求职面试:AI与大模型应用集成及云原生挑战
java·微服务·ai·kubernetes·大模型·embedding·spring ai