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

相关推荐
咖啡教室1 小时前
程序员应该掌握的网络命令telnet、ping和curl
运维·后端
你的人类朋友1 小时前
Let‘s Encrypt 免费获取 SSL、TLS 证书的原理
后端
老葱头蒸鸡1 小时前
(14)ASP.NET Core2.2 中的日志记录
后端·asp.net
YCOSA20252 小时前
ISO 雨晨 26200.6588 Windows 11 企业版 LTSC 25H2 自用 edge 140.0.3485.81
前端·windows·edge
小白呀白2 小时前
【uni-app】树形结构数据选择框
前端·javascript·uni-app
吃饺子不吃馅2 小时前
深感一事无成,还是踏踏实实做点东西吧
前端·svg·图形学
李昊哲小课2 小时前
Spring Boot 基础教程
java·大数据·spring boot·后端
码事漫谈2 小时前
C++内存越界的幽灵:为什么代码运行正常,free时却崩溃了?
后端
Swift社区2 小时前
Spring Boot 3.x + Security + OpenFeign:如何避免内部服务调用被重复拦截?
java·spring boot·后端
90后的晨仔2 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端