Day24 | Java泛型通配符与边界解析

在实际的开发过程中,我们会发现,简单的泛型(比如List<T>)在某些场景下有点死板。

比如,一个接受List<Number>的方法,没办法接收List<Integer>类型的参数。

尽管我们都知道Integer是Number的子类。

为了解决这种问题,Java泛型提供了更强大的工具------通配符 。

今天,我们一起来看看这三个东西:<?>、<? extends T>和<? super T>。

一、为什么需要通配符?

一般有需求才会催生出相应的设计,我们直接看个例子:

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName Day24Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 14:00
 * @Version 1.0
 */
public class Day24Demo {
    public static void printNumbers(List<Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<Double> doubleList = new ArrayList<>(Arrays.asList(1.0, 2.0, 3.0));
        List<Number> numberList = new ArrayList<>(Arrays.asList(1, 2.0, 3L));
        printNumbers(numberList);
        printNumbers(integerList);
        printNumbers(doubleList);
    }
}

我们本想写个方法,用来打印一个装有任意数字的列表。

实际情况是integerList和doubleList出现了编译错误。

其实Java泛型有一个很重要的概念,泛型类型之间没有继承关系。

就算Integer是Number的子类,List<Integer>也不是List<Number>的子类。它们是两种完全不同的类型。

这种类型限制就是为了保证类型安全。

如果List<Integer>可以被当成List<Number>,我们就可以通过List<Number>的引用往这个列表里添加一个Double,然后List<Integer>就不单纯的只能存放Integer了。

但是,我们又确实有"处理某一类泛型"的需求,所以通配符就来了。

二、无界通配符:<?>

<?>表示的是无界通配符,它代表"任何未知的类型"。List<?>的字面意思就是"一个持有某种未知类型的列表"。

还是来看上面的打印列表的例子:

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName Day24Demo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 14:10
 * @Version 1.0
 */
public class Day24Demo2 {
    public static void printList(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<String> stringList = new ArrayList<>(Arrays.asList("A", "B", "C"));

        printList(integerList);
        printList(stringList);
    }
}

printList方法现在可以接受任何类型的List了。

<?>的核心限制就是只读,不可写。

List<?>最大的特点就是,你不能往这个列表添加任何元素(除了null)。

因为编译器只知道它是一个列表,但不知道里面具体是什么类型。

它没办法保证你添加的元素符合列表的原始类型约束。

你可以尝试往List<?>放元素试试:

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName Day24Demo3
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 14:14
 * @Version 1.0
 */
public class Day24Demo3 {
    public static void main(String[] args) {
        List<?> list = new ArrayList<Integer>();
        list.add(1);
        list.add("Hello");
        list.add(new Object());
        list.add(null);

        Object o = list.get(0);
    }
}

你会发现,除了null以外,什么都放不进去,都是编译错误。

所以,只有当你只需要读取元素,不需要修改集合,并且处理的逻辑不依赖于元素的具体类型的时候,才会选择使用<?>。

三、上界通配符:<? extends T>

<? extends T>表示的是"任何T的子类,或者T本身"。

有了这个通配符,我们之前打印数字的方法就没问题了。

List<? extends Number>的意思是"一个持有Number或其任意子类(比如Integer,Double,Long)的列表"。

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName Day24Demo4
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 14:33
 * @Version 1.0
 */
public class Day24Demo4 {
    public static void printNumbers(List<? extends Number> list) {
        for (Number n : list) {
            System.out.println(n.doubleValue());
        }
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<Double> doubleList = new ArrayList<>(Arrays.asList(1.0, 2.0, 3.0));

        printNumbers(integerList);
        printNumbers(doubleList);
        
        // 写示例
        List<? extends Number> list = new ArrayList<>();
        list.add(1);
        list.add(1.0);
        list.add(null);
    }
}

在写示例里,我们尝试往List<? extends T>添加元素,但是<?>一样,编译错误了。

因为编译器它只知道列表里的元素是Number的某个子类,但没办法确定具体是哪个子类。

可能是List<Integer>,也可能是List<Double>。

如果让你添加进去一个Integer,万一这个列表实际上是List<Double> 类型呢?

同样会造成类型不安全。

所以上界通配符<? extends T>主要用于安全的读取数据。

四、下界通配符:<? super T>

下界通配符跟上界通配符相反,<? super T>表示"任何T的父类,或者T本身"。

List<? super Integer>的意思是"一个持有Integer或其任意父类(比如Number, Object)的列表"。

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName Day24Demo5
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 14:42
 * @Version 1.0
 */
public class Day24Demo5 {
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
    }
    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        addIntegers(integerList);
        addIntegers(numberList);
        addIntegers(objectList);
    }
}

从上面的例子里看出,不管列表是List<Integer>、List<Number>还是List<Object>,你都可以添加一个 Integer对象,这是类型安全的,因为它符合所有这些可能类型的约束。

但是,如果你从List<? super Integer>里读取元素,你没办法确定具体会得到什么类型的对象。

可能是Integer,可能是Number,也可能是Object。

所以,为了类型安全,你只能用Object类型的引用来接收取出来的元素。

java 复制代码
List<Number> numList = new ArrayList<>();
numList.add(1);
numList.add(2.5);

List<? super Integer> list = numList;
Object o1 = list.get(0);
Integer i1 = list.get(0);

"Integer i1 = list.get(0);"会报编译错误。

所以,下界通配符<? super T>一般都用来安全地写入数据。

五、PECS原则

初学的时候,我们可能不知道什么时候该使用extends,什么时候该使用super。

设计Java集合框架的Joshua Bloch提出了一个很著名的原则:PECS。

Producer Extends:

如果你的方法需要一个泛型集合作为参数,而且这个方法主要是从这个集合里读取(生产)数据,那就用 <? extends T>。

Consumer Super:

如果你的方法主要是往这个集合里写入(添加、消费)数据,那就使用<? super T>。

在Java标准库里,有这样一个方法:Collections.copy()。

这个方法其实就是PECS的具体体现。

来看一下这个方法的源码签名:

java 复制代码
public static <T> void copy(List<? super T> dest, List<? extends T> src)

参数里的src是数据的生产者 (Producer)。

我们要从src里读取元素,所以用<? extends T>。

这样我们可以从src里安全地取出T和其子类型的对象。

参数里的dest是数据的消费者 (Consumer)。

我们要把元素写入到dest里,所以使用<? super T>。

这样我们就可以安全地把T类型的对象(从src中取出的)添加到dest列表里。

java 复制代码
package com.lazy.snail.day24;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @ClassName Day24Demo6
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/18 15:23
 * @Version 1.0
 */
public class Day24Demo6 {
    public static void main(String[] args) {
        List<Integer> src = Arrays.asList(1, 2, 3);
        List<Number> dest = new ArrayList<>();
        dest.add(0.0);
        dest.add(0.0);
        dest.add(0.0);
        Collections.copy(dest, src);
        System.out.println(dest);
    }
}

上面的代码里,我们把一个List<Integer>拷贝到了一个List<Number>里。

dest在这里是List<Number>,符合<? super Integer>。

然后src在这里是List<Integer>,符合<? extends Integer>。

所以Collections.copy才能执行成功。

结语

在Java的集合框架中,大量的使用了泛型以及泛型通配符。

Java的集合框架是Java比较重要的一个版块。

了解及掌握了泛型和通配符对我们后续的集合学习非常有帮助。

下一篇预告

Day25 | 为什么说Java的泛型是伪泛型

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

本文首发于知乎专栏************************************************************************************************************************************************************************************************************************************************************《Java 100天成长计划》****************************************************************************************************************************************************************************************************************************************************************

相关推荐
Query*6 分钟前
Java 设计模式——代理模式:从静态代理到 Spring AOP 最优实现
java·设计模式·代理模式
梵得儿SHI7 分钟前
Java 反射机制深度解析:从对象创建到私有成员操作
java·开发语言·class对象·java反射机制·操作类成员·三大典型·反射的核心api
JAVA学习通11 分钟前
Spring AI 核心概念
java·人工智能·spring·springai
望获linux13 分钟前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统
绝无仅有21 分钟前
面试真实经历某商银行大厂数据库MYSQL问题和答案总结(二)
后端·面试·github
绝无仅有23 分钟前
通过编写修复脚本修复 Docker 启动失败(二)
后端·面试·github
..Cherry..25 分钟前
【java】jvm
java·开发语言·jvm
老K的Java兵器库34 分钟前
并发集合踩坑现场:ConcurrentHashMap size() 阻塞、HashSet 并发 add 丢数据、Queue 伪共享
java·后端·spring
冷冷的菜哥1 小时前
go邮件发送——附件与图片显示
开发语言·后端·golang·邮件发送·smtp发送邮件
向葭奔赴♡1 小时前
Spring Boot 分模块:从数据库到前端接口
数据库·spring boot·后端