乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

通配符与PECS原则

"通配符 ? 是泛型世界的'外交官'——它让不同类型参数化的泛型类能够互通有无。掌握PECS原则,你就掌握了泛型设计的终极心法。" —— 白歌,飞翔科技架构师


1. 问题的提出:泛型不是协变的

小崔在飞翔科技写了一个方法,用于打印学生列表:

public void printStudents(List<Student> students) {
    for (Student s : students) {
        System.out.println(s.getName());
    }
}

一切正常,直到有一天他需要打印CollegeStudent(Student的子类)列表:

List<CollegeStudent> collegeStudents = new ArrayList<>();
collegeStudents.add(new CollegeStudent("小崔", 22, "清华大学"));

printStudents(collegeStudents);  // ❌ 编译错误!

编译错误:The method printStudents(List<Student>) is not applicable for the arguments (List<CollegeStudent>)

小崔困惑了:CollegeStudent extends Student,为什么List<CollegeStudent>不是List<Student>的子类型?

不可协变的原因

虽然CollegeStudent是Student的子类,但List<CollegeStudent>不是List<Student>的子类。为什么?看下面的例子:

// 假设泛型是协变的(实际上不是!)
// List<CollegeStudent> cs = new ArrayList<>();
// List<Student> students = cs;     // 假设允许这样赋值
// students.add(new HighSchoolStudent("张三", 16)); // 放入一个非CollegeStudent的Student

// 那么 cs.get(0) 返回的就会是一个 HighSchoolStudent,但 cs 被声明为
// List<CollegeStudent> —— 类型系统被破坏了!

2. 通配符 ?:泛型世界的救星

通配符(Wildcard)用?表示"未知类型",用于在泛型中表达更灵活的类型关系。

通配符三兄弟

通配符名称读能力写能力典型场景
<?>无界通配符读出为Object不能写(除null)List<?> 作为"只读"集合
<? extends T>上界通配符读出为T不能写(除null)生产者——从集合中读取T
<? super T>下界通配符读出为Object可以写入T及其子类消费者——向集合中写入T

一句话总结


3. 上界通配符 <? extends T>:生产者

<? extends T>表示"T或T的某个子类型",具体是哪个子类型未知。由此产生可读不可写的特性。

语法与规则

// 方法参数使用上界通配符 —— 支持所有 Student 的子类型列表
public void printStudents(List<? extends Student> students) {
    for (Student s : students) {        // ✅ 读取:安全,返回类型是Student
        System.out.println(s.getName());
    }
    // students.add(new Student(...));  // ❌ 写入:编译错误!不知道具体是什么子类型
    // students.add(new CollegeStudent(...)); // ❌ 同样编译错误!
    students.add(null);                 // ✅ 只有null可以添加
}

为什么不能写? 因为List<? extends Student>可能是List<CollegeStudent>,也可能是List<HighSchoolStudent>。如果允许add(Student),就可能把HighSchoolStudent放入List<CollegeStudent>中——类型不安全。

完整示例:飞翔科技的成绩单系统

/**
 * 飞翔科技成绩计算服务
 * 使用上界通配符处理各种学生子类型
 */
public class GradeService {

    /**
     * 计算学生列表的平均成绩
     * <? extends Student> 接受 Student 及其任意子类的列表
     */
    public static double calculateAverage(List<? extends Student> students) {
        if (students.isEmpty()) return 0.0;

        double total = 0.0;
        for (Student s : students) {      // 安全读出为 Student
            total += s.getScore();
        }
        return total / students.size();
    }

    /**
     * 获取成绩最高的学生
     */
    public static Student getTopStudent(List<? extends Student> students) {
        if (students.isEmpty()) return null;

        Student top = students.get(0);
        for (Student s : students) {
            if (s.getScore() > top.getScore()) {
                top = s;
            }
        }
        return top;
    }
}
// 测试代码
public class GradeServiceTest {
    public static void main(String[] args) {
        // CollegeStudent 列表
        List<CollegeStudent> collegeStudents = new ArrayList<>();
        collegeStudents.add(new CollegeStudent("小崔", 22, "清华大学", 92.5));
        collegeStudents.add(new CollegeStudent("白歌", 28, "北京大学", 88.0));
        collegeStudents.add(new CollegeStudent("大翔", 35, "浙江大学", 95.5));

        // HighSchoolStudent 列表
        List<HighSchoolStudent> highSchoolStudents = new ArrayList<>();
        highSchoolStudents.add(new HighSchoolStudent("小明", 16, "飞翔中学", 85.0));
        highSchoolStudents.add(new HighSchoolStudent("小红", 17, "飞翔中学", 91.0));

        // 同一个方法处理两种不同类型的列表
        System.out.println("大学生平均成绩:" + 
            GradeService.calculateAverage(collegeStudents));
        System.out.println("中学生平均成绩:" + 
            GradeService.calculateAverage(highSchoolStudents));

        System.out.println("\n大学生最高分:" + 
            GradeService.getTopStudent(collegeStudents).getName());
        System.out.println("中学生最高分:" + 
            GradeService.getTopStudent(highSchoolStudents).getName());
    }
}

运行输出:

大学生平均成绩:92.0
中学生平均成绩:88.0

大学生最高分:大翔
中学生最高分:小红

List<? extends Number> 的读写行为

List<? extends Number> numbers = new ArrayList<Integer>();  // 可以赋值为任何Number子类的列表

Number n = numbers.get(0);   // ✅ 读:返回 Number 类型
// Integer i = numbers.get(0); // ❌ 编译错误!不能确定具体是Integer还是Double

// numbers.add(100);           // ❌ 编译错误!
// numbers.add(3.14);          // ❌ 编译错误!
numbers.add(null);             // ✅ 只有 null 可以

4. 下界通配符 <? super T>:消费者

<? super T>表示"T或T的某个超类型",由此产生可写不可读(读只能到Object)的特性。

语法与规则

// 方法参数使用下界通配符 —— 可以向集合中安全地放入 Student 对象
public void addStudents(List<? super Student> students) {
    students.add(new Student("小崔", 22));         // ✅ 写入:安全
    students.add(new CollegeStudent("白歌", 28, "北大"));  // ✅ 子类也可以

    // Student s = students.get(0);  // ❌ 编译错误!返回类型是 Object(或捕获类型)
    Object obj = students.get(0);    // ✅ 只能读为 Object
}

为什么能写但不能读? 因为List<? super Student>可能是List<Student>,也可能是List<Object>。写入Student是安全的(Student是Student自身,也是Object的子类),但读取时无法确定返回类型是Student还是Object——最安全的做法是返回Object。

完整示例:飞翔科技的数据收集器

/**
 * 飞翔科技数据收集服务
 * 使用下界通配符向集合中填充数据
 */
public class DataCollector {

    /**
     * 将源列表中的元素复制到目标列表
     * <? super T> 目标集合是消费者 —— 接受 T 及其超类的列表
     * <? extends T> 源集合是生产者 —— 提供 T 及其子类
     */
    public static <T> void copyInto(
            List<? super T> dest,      // 消费者:写入
            List<? extends T> src) {   // 生产者:读取
        for (T item : src) {
            dest.add(item);            // 安全写入 —— dest 接受 T 及其超类
        }
    }

    /**
     * 批量添加学生到集合
     * <? super Student> 接受 Student、Object 等超类的集合
     */
    public static void addAllStudents(
            List<? super Student> target,
            Student... students) {
        for (Student s : students) {
            target.add(s);
        }
    }
}
// 测试代码
public class DataCollectorTest {
    public static void main(String[] args) {
        // === 测试 <? super Student> ===
        List<Object> objList = new ArrayList<>();    // Object 是 Student 的超类
        DataCollector.addAllStudents(objList,
            new Student("小崔", 22, 88.0),
            new CollegeStudent("白歌", 28, 95.0)
        );
        System.out.println("Object列表中的元素:");
        for (Object obj : objList) {
            System.out.println("  " + obj);
        }

        // === 测试 copyInto ===
        List<CollegeStudent> srcList = new ArrayList<>();
        srcList.add(new CollegeStudent("大翔", 35, "浙江大学", 92.0));
        srcList.add(new CollegeStudent("小崔", 22, "清华大学", 88.0));

        List<Student> destList = new ArrayList<>();   // Student 是 CollegeStudent 的超类
        DataCollector.copyInto(destList, srcList);
        System.out.println("\n复制结果:");
        for (Student s : destList) {
            System.out.println("  " + s.getName() + " - " + s.getScore() + "分");
        }
    }
}

运行输出:

Object列表中的元素:
  Student{name='小崔', age=22, score=88.0}
  CollegeStudent{name='白歌', age=28, college='null', score=95.0}

复制结果:
  大翔 - 92.0分
  小崔 - 88.0分

5. PECS原则

PECS 是 Josh Bloch 在《Effective Java》中提出的泛型使用原则:

Producer Extends, Consumer Super

角色通配符含义操作
Producer(生产者)<? extends T>从结构中读取T类型数据只读不写
Consumer(消费者)<? super T>向结构中写入T类型数据只写不读

判断方法

问自己一个问题:这个参数是用来读(获取数据)还是写(放入数据)?

  • 如果只读:用 <? extends T>
  • 如果只写:用 <? super T>
  • 如果又读又写:用精确类型 <T>(不要用通配符)

PECS决策流程图

JDK中的PECS经典案例

Collections.copy —— PECS的教科书级应用

// JDK源码中的签名
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // dest:消费者(写入),用 <? super T>
    // src:生产者(读取),用 <? extends T>
    for (int i = 0; i < srcSize; i++) {
        dest.set(i, src.get(i));
    }
}

使用示例:

List<Object> dest = new ArrayList<>(Arrays.asList(null, null, null));
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src);  // Integer 可以放入 List<Object>——安全!
System.out.println(dest);     // [1, 2, 3]

Java 8 Stream API

// Predicate —— 生产者(判断条件)
Stream<T> filter(Predicate<? super T> predicate);  // 传入 T 或其超类的谓词

// Consumer —— 消费者(执行操作)
void forEach(Consumer<? super T> action);          // 接受 T 或其超类的操作

// Function —— 生产+消费
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
//                    输入是消费者(super)   输出是生产者(extends)

飞翔科技完整案例:泛型Service层

/**
 * 飞翔科技通用的CRUD服务接口
 * 展示PECS原则在实际业务中的运用
 */
public interface Service<T> {

    /**
     * PECS展示:从集合中读取数据进行处理
     * items 是生产者 —— 只读取,用 extends
     */
    default List<T> filterByCondition(
            List<? extends T> items,       // 生产者:读取T数据
            Predicate<? super T> condition // 消费者:传入T进行判断
    ) {
        List<T> result = new ArrayList<>();
        for (T item : items) {             // 从 extends 集合安全读取
            if (condition.test(item)) {    // 用 super 条件判断
                result.add(item);
            }
        }
        return result;
    }

    /**
     * PECS展示:向集合中写入数据
     * target 是消费者 —— 只写入,用 super
     */
    default void collectInto(
            List<? super T> target,        // 消费者:写入T数据
            T... items
    ) {
        for (T item : items) {
            target.add(item);              // 向 super 集合安全写入
        }
    }
}
// 测试代码
public class ServiceTest {
    public static void main(String[] args) {
        Service<Student> studentService = new Service<Student>() {};

        // === 测试 filterByCondition ===
        List<CollegeStudent> students = new ArrayList<>();
        students.add(new CollegeStudent("小崔", 22, "清华", 92.0));
        students.add(new CollegeStudent("白歌", 28, "北大", 78.0));
        students.add(new CollegeStudent("大翔", 35, "浙大", 95.0));

        // 过滤成绩 >= 90 的学生
        // students: List<CollegeStudent> 是 List<? extends Student>
        // condition: Predicate<Object> > Predicate<Student>,可以用 Predicate<? super Student>
        Predicate<Object> highScore = s -> 
            s instanceof Student && ((Student) s).getScore() >= 90;
        
        List<Student> topStudents = studentService.filterByCondition(students, highScore);
        System.out.println("高分学生:");
        topStudents.forEach(s -> System.out.println("  " + s.getName() + ": " + s.getScore()));

        // === 测试 collectInto ===
        List<Object> collector = new ArrayList<>();  // Object 是 Student 的超类
        studentService.collectInto(collector,
            new Student("新生A", 20, 85.0),
            new Student("新生B", 21, 90.0)
        );
        System.out.println("\n收集结果:");
        collector.forEach(System.out::println);
    }
}

运行输出:

高分学生:
  小崔: 92.0
  大翔: 95.0

收集结果:
Student{name='新生A', age=20, score=85.0}
Student{name='新生B', age=21, score=90.0}

6. 无界通配符 <?>

<?>表示"任意类型的泛型"。它与原始类型List不同——List<?>仍然是泛型,编译器会对它进行类型安全检查。

List<?> vs 原始类型 List

特性List<?>List(原始类型)
读取返回Object返回Object
写入不能写(除null)可以写任意类型(不安全)
编译检查泛型检查生效泛型检查完全关闭
赋值安全安全不安全(警告)
List<?> wildList = new ArrayList<String>();
// wildList.add("hello");     // ❌ 编译错误!不能写入
// wildList.add(123);         // ❌ 编译错误!
wildList.add(null);           // ✅ 只有 null 可以
Object obj = wildList.get(0); // ✅ 读取为 Object

// —— 对比 ——

List rawList = new ArrayList<String>();
rawList.add("hello");         // ✅ 编译通过(仅警告)
rawList.add(123);             // ✅ 编译通过(仅警告)—— 危险!

使用场景

<?>适用于以下场景:

  1. 方法不依赖具体类型:如List.size()、List.clear()
  2. 只使用Object方法:如toString()、equals()
  3. 类型检查:obj instanceof List<?>
/**
 * 不需要知道元素类型就能完成的操作
 */
public class WildcardUtils {

    // ✅ 只使用 List 自身方法,不涉及元素类型
    public static boolean isEmpty(List<?> list) {
        return list == null || list.isEmpty();
    }

    // ✅ 只需要 Object 方法
    public static void printAll(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }

    // ✅ 安全类型检查
    public static boolean isList(Object obj) {
        return obj instanceof List<?>;
    }
}

7. 通配符捕获

通配符捕获(Wildcard Capture)是编译器的一种智能推断机制:当操作通配符类型时,编译器内部会创建一个"捕获类型"来替代?。

捕获问题

public static void swapFirstTwo(List<?> list) {
    // ❌ 以下代码无法编译!
    // Object first = list.get(0);
    // Object second = list.get(1);
    // list.set(0, second);  // 编译错误!
    // list.set(1, first);   // 编译错误!
}

原因:编译器将List<?>的?捕获为一个未知类型#CAP1,而set(int, #CAP1)要求传入的类型必须是#CAP1——但list.get(0)返回的是#CAP1,list.set(0, list.get(1))应该合法才对啊?

问题在于编译器的捕获类型机制不够智能,无法推断出list.get(0)和list.get(1)的返回类型是同一个#CAP1。

解决方案:辅助泛型方法

// ✅ 方案:用泛型方法捕获通配符类型
public static void swapFirstTwo(List<?> list) {
    swapHelper(list);  // 编译器将 ? 捕获为具体类型 T
}

// 辅助泛型方法 —— T 捕获了通配符 ?
private static <T> void swapHelper(List<T> list) {
    T first = list.get(0);
    T second = list.get(1);
    list.set(0, second);  // ✅ 类型安全:T ↔ T
    list.set(1, first);
}

完整验证示例

public class WildcardCaptureDemo {

    /**
     * 公开展示的通配符版本
     */
    public static void reverse(List<?> list) {
        reverseHelper(list);  // 通配符捕获
    }

    /**
     * 私有的泛型辅助方法 —— T 捕获 ?
     */
    private static <T> void reverseHelper(List<T> list) {
        int size = list.size();
        for (int i = 0; i < size / 2; i++) {
            T temp = list.get(i);
            list.set(i, list.get(size - 1 - i));
            list.set(size - 1 - i, temp);
        }
    }

    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("大翔", "白歌", "小崔"));
        System.out.println("反转前:" + names);
        reverse(names);
        System.out.println("反转后:" + names);

        List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        System.out.println("\n反转前:" + nums);
        reverse(nums);
        System.out.println("反转后:" + nums);
    }
}

运行输出:

反转前:[大翔, 白歌, 小崔]
反转后:[小崔, 白歌, 大翔]

反转前:[1, 2, 3, 4, 5]
反转后:[5, 4, 3, 2, 1]

8. 通配符使用最佳实践


9. 易错场景

反例一:把 ? extends T 当容器往里装数据

// ❌ 错误:试图向 <? extends T> 集合添加元素
public void addTopStudent(List<? extends Student> students) {
    Student top = findTop(students);
    students.add(new Student("新生", 20, 0));  // ❌ 编译错误!
    // 即使放回同一个对象也不行
    students.add(top);                          // ❌ 编译错误!
}

纠正:

// ✅ 正确:用 ? super T 或精确类型
public void addTopStudent(List<Student> students) {
    Student top = findTop(students);
    students.add(new Student("新生", 20, 0));  // ✅ 精确类型——可读可写
}

// 或者只获取不修改
public Student findTop(List<? extends Student> students) {
    return students.stream()
        .max(Comparator.comparingDouble(Student::getScore))
        .orElse(null);
}

反例二:混淆 ? extends 和 ? super 的读写方向

// ❌ 错误:从 <? super Student> 读取时以为是Student类型
public void printNames(List<? super Student> list) {
    for (Student s : list) {  // ❌ 编译错误!不能当作Student遍历
        System.out.println(s.getName());
    }
}

纠正:

// ✅ 正确:<? super Student> 只能以 Object 读取
public void printNames(List<? super Student> list) {
    for (Object obj : list) {  // ✅ 只能确保是 Object
        if (obj instanceof Student) {
            Student s = (Student) obj;
            System.out.println(s.getName());
        }
    }
}
// 如果需要以 Student 类型读取,应该使用 ? extends Student 或精确类型

反例三:用通配符声明返回类型

// ❌ 不推荐:返回类型使用通配符 —— 调用方使用困难
public List<? extends Student> getStudents() {
    List<CollegeStudent> list = new ArrayList<>();
    // ...
    return list;
}
// 调用方:
List<? extends Student> result = service.getStudents();
// result.add(new Student(...));  // ❌ 无法写入!调用方可能也需要写入

纠正:

// ✅ 正确:返回精确类型,参数使用通配符
public <T extends Student> List<T> getStudents(Class<T> type) {
    List<T> list = new ArrayList<>();
    // ...
    return list;
}
// 调用方:
List<CollegeStudent> result = service.getStudents(CollegeStudent.class);
result.add(new CollegeStudent(...));  // ✅ 可以正常使用

10. 面试考点

Q1:List<? extends T> 和 List<? super T> 有什么区别?

List<? extends T>(上界通配符):只能安全地读取T类型数据,不能写入(除null)。用于生产者场景——从集合获取数据。List<? super T>(下界通配符):可以安全地写入T类型数据,读取只能到Object。用于消费者场景——向集合存放数据。

Q2:什么是PECS原则?

PECS = Producer Extends, Consumer Super。如果一个参数化类型是生产者(为你提供数据),用<? extends T>;如果是消费者(接收你的数据),用<? super T>。这是Josh Bloch在《Effective Java》中提出的泛型使用核心原则。JDK中Collections.copy的签名就是教科书级的PECS应用。

Q3:List<?> 和 List(原始类型)有什么区别?

List<?>是泛型类型,编译器会进行类型安全检查——不能写入任何非null元素。List是原始类型,完全退出泛型检查——可以写入任何对象(仅警告),可以在类型间不安全转换。List<?>的安全性远高于原始类型。在需要"任意类型的List"时,始终使用List<?>而非原始类型。

Q4:为什么 List<Object> 不能接收 List<String> 参数?

因为Java泛型是不可协变的(invariant)。虽然String是Object的子类,但List<String>不是List<Object>的子类型。如果允许这样做,就可以向List<String>中放入非String对象,破坏类型安全。解决方案是使用通配符:方法参数声明为List<? extends Object>(等价于List<?>)来接受任意类型的List。

Q5:通配符捕获是什么?如何使用?

通配符捕获是编译器的内部机制,将List<?>中的?替换为一个内部捕获类型。当需要对List<?>执行"取出-放入"操作时(如交换两个元素),直接使用通配符会受到限制。解决方法是定义一个私有泛型辅助方法<T> void helper(List<T> list),让编译器通过方法调用完成捕获推断。

上一页
泛型基础
下一页
类型擦除