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 泛型,提高代码的灵活性和可维护性。🚀

相关推荐
hqxstudying1 小时前
J2EE模式---前端控制器模式
java·前端·设计模式·java-ee·状态模式·代码规范·前端控制器模式
Microsoft Word1 小时前
用户中心项目实战(springboot+vue快速开发管理系统)
vue.js·spring boot·后端
开开心心就好2 小时前
Excel数据合并工具:零门槛快速整理
运维·服务器·前端·智能手机·pdf·bash·excel
im_AMBER2 小时前
Web开发 05
前端·javascript·react.js
Au_ust2 小时前
HTML整理
前端·javascript·html
安心不心安3 小时前
npm全局安装后,依然不是内部或外部命令,也不是可运行的程序或批处理文件
前端·npm·node.js
不写八个3 小时前
GoLang教程005:switch分支
开发语言·后端·golang
迷曳4 小时前
28、鸿蒙Harmony Next开发:不依赖UI组件的全局气泡提示 (openPopup)和不依赖UI组件的全局菜单 (openMenu)、Toast
前端·ui·harmonyos·鸿蒙
爱分享的程序员4 小时前
前端面试专栏-工程化:29.微前端架构设计与实践
前端·javascript·面试
上单带刀不带妹4 小时前
Vue3递归组件详解:构建动态树形结构的终极方案
前端·javascript·vue.js·前端框架