139. Java 泛型 - Java 通配符使用准则

139. Java 泛型 - Java 通配符使用准则

在 Java 泛型编程中,通配符(?)的使用是一个让许多开发者感到困惑的话题。特别是在决定何时使用**上限通配符(? extends T 下限通配符(? super T)**时,更需要遵循一些准则来确保代码的可读性和安全性。


1. "in" 和 "out" 原则

在确定是否使用通配符以及选择适当的通配符时,可以采用 "in" 和 "out" 原则 ,这也是**PECS**(Producer Extends, Consumer Super)的核心思想:

  • "in" 变量(输入变量)
    • 提供数据 供方法使用(Producer)。
    • 使用 extends 关键字(上限通配符)。
    • 例如:List<? extends Number> 表示这个列表只能读取 Number 或其子类的元素,但不能添加新的元素(除 null 外)。
  • "out" 变量(输出变量)
    • 存储数据 以供方法输出(Consumer)。
    • 使用 super 关键字(下限通配符)。
    • 例如:List<? super Integer> 表示这个列表可以存储 Integer 或其超类的元素,但读取时只能作为 Object 处理。
  • 既是 "in" 又是 "out" 的变量
    • 不能使用通配符,应使用明确的泛型类型
    • 例如:List<T>,如果方法既要读取 又要写入 元素,使用具体的类型 T 而不是通配符。

2. 何时使用 extends(上限通配符)

当一个变量仅作为输入数据Producer)时,应使用 ? extends T,它表示该变量是 T 或 T 的子类型

示例 1:使用 extends 处理只读数据

java 复制代码
import java.util.Arrays;
import java.util.List;

public class UpperBoundExample {
    public static double sum(List<? extends Number> numbers) {
        double sum = 0;
        for (Number num : numbers) {
            sum += num.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

        System.out.println(sum(intList));  // ✅ 输出:6.0
        System.out.println(sum(doubleList)); // ✅ 输出:6.6
    }
}
解析
  • List<? extends Number> 允许 IntegerDoubleNumber 子类的列表作为参数传递。
  • 但是,不能向 numbers 列表添加元素 (除了 null),因为编译器无法确定它具体是什么类型。

示例 2:为什么不能向 ? extends T 添加元素?

java 复制代码
List<? extends Number> numList = new ArrayList<>();
numList.add(5);  // ❌ 编译错误
原因
  • ? extends Number 表示该列表可能是 List<Integer>List<Double> 等,但 Java 无法确定具体类型。
  • 因此,不能向 numList 添加元素(除了 null),以避免潜在的类型错误。

3. 何时使用 super(下限通配符)

当一个变量仅用于存储数据Consumer)时,应使用 ? super T,它表示该变量是 TT 的超类

示例 3:使用 super 处理可写入数据

java 复制代码
import java.util.List;
import java.util.ArrayList;

public class LowerBoundExample {
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        System.out.println(numList); // ✅ 输出:[1, 2, 3]

        List<Object> objList = new ArrayList<>();
        addNumbers(objList);
        System.out.println(objList); // ✅ 输出:[1, 2, 3]
    }
}
解析
  • List<? super Integer> 允许 IntegerNumberObject 类型的列表作为参数传递。
  • 这样,方法可以安全地向列表添加 Integer 类型的元素 ,因为 IntegerTT 的子类型。
  • 但是,读取时只能作为 Object 处理,因为编译器无法确定它具体存储的是什么类型。

示例 4:为什么不能从 ? super T 读取具体类型?

java 复制代码
List<? super Integer> list = new ArrayList<>();
list.add(42); // ✅ 可以添加 Integer
Integer num = list.get(0); // ❌ 编译错误
原因
  • ? super Integer 可能是 List<Integer>List<Number>List<Object>
  • 读取 list.get(0) 时,编译器只能保证它是 Object,但不能确定它是 Integer,所以不能直接赋值给 Integer

4. 何时使用无界通配符 ?

如果变量可以使用 Object 的方法(如 toString()equals()),但不涉及泛型特定的操作,使用无界通配符 ? 更合适。

示例 5:无界通配符适用于打印数据

java 复制代码
import java.util.List;

public class UnboundedExample {
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        List<String> strList = List.of("A", "B", "C");
        List<Integer> intList = List.of(1, 2, 3);
        
        printList(strList); // ✅ 输出 A B C
        printList(intList); // ✅ 输出 1 2 3
    }
}
解析
  • List<?> 允许任何类型List 作为参数。
  • 但只能将元素当作 Object 读取,不能进行写操作(除了 null)。

5. 避免在返回类型中使用通配符

避免在方法的返回类型中使用通配符,否则调用者无法确定返回值的确切类型。

错误示例

java 复制代码
public static List<? extends Number> getNumbers() {
    return new ArrayList<Integer>(); 
}
问题
  • 方法的返回类型是 List<? extends Number>,但调用者不知道它的具体类型,无法向列表添加元素。
  • 可能导致不必要的类型转换错误。

更好的做法

java 复制代码
public static List<Number> getNumbers() {
    return new ArrayList<>();
}

这样,返回类型更清晰,调用者可以正确操作列表中的元素。


6. 结论

情况 关键字 作用
读取数据 ? extends T 上限通配符,适用于"Producer"(提供数据)
写入数据 ? super T 下限通配符,适用于"Consumer"(消费数据)
仅使用 Object 方法 ? 适用于泛型方法中不关心类型的情况
同时读取和写入 具体类型 T 避免使用通配符

掌握这些规则,可以更安全、更高效地使用 Java 泛型,提高代码的灵活性和可维护性。🚀

相关推荐
柏油12 分钟前
Spring @TransactionalEventListener 解读
spring boot·后端·spring
小离a_a32 分钟前
使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
前端·css
郭优秀的笔记1 小时前
抽奖程序web程序
前端·css·css3
布兰妮甜1 小时前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
小小愿望2 小时前
ECharts 实战技巧:揭秘 X 轴末项标签 “莫名加粗” 之谜及破解之道
前端·echarts
两码事2 小时前
告别繁琐的飞书表格API调用,让飞书表格操作像操作Java对象一样简单!
java·后端
小小愿望2 小时前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css
fail_to_code2 小时前
请不要再只会回答宏任务和微任务了
前端
摸着石头过河的石头2 小时前
taro3.x-4.x路由拦截如何破?
前端·taro
lpfasd1232 小时前
开发Chrome/Edge插件基本流程
前端·chrome·edge