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天成长计划》****************************************************************************************************************************************************************************************************************************************************************

相关推荐
HezhezhiyuLe2 小时前
MAC idea 环境变量设置失效
java·macos·intellij-idea
fatfishccc3 小时前
(七)API 重构的艺术:打造优雅、可维护的 API
java·驱动开发·intellij-idea·软件研发·后端开发·代码重构·api重构
Eoch773 小时前
从买菜到秒杀:Redis为什么能让你的网站快如闪电?
java·后端
我不是混子3 小时前
奇葩面试题:线程调用两次start方法会怎样?
java·后端
凤年徐3 小时前
【C++模板编程】从泛型思想到实战应用
java·c语言·开发语言·c++
摸鱼总工3 小时前
为什么读源码总迷路?有破解办法吗
后端
仙俊红3 小时前
深入理解 ThreadLocal —— 在 Spring Boot 中的应用与原理
java·spring boot·后端
飞鱼&4 小时前
RabbitMQ-高可用机制
java·rabbitmq·java-rabbitmq