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 对象。所以,这个写入操作的安全性,同样源于那个基础的赋值规则在所有可能父类型上的普遍适用性。

相关推荐
北执南念6 分钟前
项目代码生成工具
java
中国lanwp12 分钟前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
苹果酱05671 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
Java致死1 小时前
单例设计模式
java·单例模式·设计模式
胡子发芽1 小时前
请详细解释Java中的线程池(ThreadPoolExecutor)的工作原理,并说明如何自定义线程池的拒绝策略
java
沫夕残雪1 小时前
Tomcat的安装与配置
java·tomcat
胡子发芽1 小时前
请解释Java中的NIO(New I/O)与传统I/O的区别,并说明NIO中的关键组件及其作用
java
柚个朵朵2 小时前
IDEA中使用Git
java·git·spring
jerry6092 小时前
优先队列、堆笔记(算法第四版)
java·笔记·算法
666HZ6662 小时前
关于IDEA的循环依赖问题
java·ide·intellij-idea