编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

      • README
      • 基础语法
      • 数据类型
      • 运算符
      • 字符串和数组
      • 流程语句
      • 函数方法
      • 类和对象
      • 继承和多态
      • 接口和抽象类
      • 异常处理
      • 集合框架
      • IO流和File
      • 线程和锁
      • 泛型
        • 14.1 泛型入门
          • 14.1.1 为什么需要泛型
          • 14.1.2 泛型的好处
          • 14.1.3 综合案例:类型安全对比
          • 14.1.4 泛型入门训练题
        • 14.2 泛型类
          • 14.2.1 泛型类定义
          • 14.2.2 多类型参数
          • 14.2.3 综合案例:通用键值对容器
          • 14.2.4 泛型类训练题
        • 14.3 泛型方法
          • 14.3.1 泛型方法定义
          • 14.3.2 泛型方法使用
          • 14.3.3 综合案例:泛型工具方法集
          • 14.3.4 泛型方法训练题
        • 14.4 泛型接口
          • 14.4.1 泛型接口定义
          • 14.4.2 综合案例:泛型接口实现
          • 14.4.3 泛型接口训练题
        • 14.5 通配符
          • 14.5.1 无界通配符
          • 14.5.2 上界通配符
          • 14.5.3 下界通配符
          • 14.5.4 PECS原则深度解析
          • 14.5.5 综合案例:通配符使用场景
          • 14.5.6 通配符训练题
        • 14.6 类型擦除
          • 14.6.1 擦除机制
          • 14.6.2 擦除的影响
          • 14.6.3 绕过类型擦除的方式
          • 14.6.4 综合案例:类型擦除探测器
          • 14.6.5 类型擦除训练题
      • 注解和反射
    • 综合案例

    • 专栏博客

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 入门教程
杨充
2026-04-07
目录

泛型

# 14.泛型

# 目录介绍

  • 14.1 泛型入门
    • 14.1.1 为什么需要泛型
    • 14.1.2 泛型的好处
    • 14.1.3 综合案例:类型安全对比
    • 14.1.4 泛型入门训练题
  • 14.2 泛型类
    • 14.2.1 泛型类定义
    • 14.2.2 多类型参数
    • 14.2.3 综合案例:通用键值对容器
    • 14.2.4 泛型类训练题
  • 14.3 泛型方法
    • 14.3.1 泛型方法定义
    • 14.3.2 泛型方法使用
    • 14.3.3 综合案例:泛型工具方法集
    • 14.3.4 泛型方法训练题
  • 14.4 泛型接口
    • 14.4.1 泛型接口定义
    • 14.4.2 综合案例:泛型接口实现
    • 14.4.3 泛型接口训练题
  • 14.5 通配符
    • 14.5.1 无界通配符
    • 14.5.2 上界通配符
    • 14.5.3 下界通配符
    • 14.5.4 PECS原则深度解析
    • 14.5.5 综合案例:通配符使用场景
    • 14.5.6 通配符训练题
  • 14.6 类型擦除
    • 14.6.1 擦除机制
    • 14.6.2 擦除的影响
    • 14.6.3 绕过类型擦除的方式
    • 14.6.4 综合案例:类型擦除探测器
    • 14.6.5 类型擦除训练题

# 14.1 泛型入门

# 14.1.1 为什么需要泛型

没有泛型时,集合只能存 Object,取出时需要强制转换,容易出错:

// 没有泛型
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123);       // 可以放任何类型
String s = (String) list.get(1);  // 运行时 ClassCastException!

// 使用泛型
ArrayList<String> list2 = new ArrayList<>();
list2.add("Hello");
// list2.add(123);   // 编译错误!类型安全
String s2 = list2.get(0);  // 不需要强制转换
1
2
3
4
5
6
7
8
9
10
11

# 14.1.2 泛型的好处

  1. 类型安全:编译期检查类型,避免 ClassCastException。
  2. 消除强制转换:不需要手动类型转换。
  3. 代码复用:一份代码支持多种类型。

对比 C++:C++ 的模板(template)在编译时展开,为每种类型生成独立的代码。Java 的泛型通过类型擦除实现,运行时没有泛型信息。

疑惑:泛型只是用来避免强制转换吗?它的设计背景是什么?

JDK 1.4 及之前,集合框架只能存储 Object,开发者每次取出元素都要手动强制转换,这不仅繁琐而且极易出错。ClassCastException 成为 Java 最常见的运行时异常之一。

答疑:泛型的核心目标是将类型检查从运行时前移到编译时。这是一种"快速失败"(fail-fast)的设计理念——错误越早暴露,修复成本越低。编译期的类型检查比运行时的 ClassCastException 要好得多。

论证:Martin Odersky(后来创建了 Scala)在 1998 年提出了 GJ(Generic Java)提案,后来发展成 JSR 14,最终在 JDK 5(2004年)正式引入泛型。设计时面临一个关键抉择:是否向后兼容。C# 选择了不兼容(.NET 2.0 的泛型是 CLR 层面真正支持的),而 Java 选择了向后兼容,采用类型擦除方案——编译后的字节码与 JDK 1.4 完全兼容。

结果展示:这个向后兼容的决定深刻影响了 Java 泛型的设计。它带来了好处(旧代码无需修改就能运行),也带来了限制(不能创建泛型数组、不能用 instanceof 检查泛型类型等)。理解这个历史背景,才能真正理解泛型的种种"奇怪"限制。

# 14.1.3 综合案例:类型安全对比

本案例对比无泛型和有泛型的代码,直观展示泛型的好处。

import java.util.*;

/**
 * 类型安全对比 —— 泛型入门综合案例
 * 知识点:原始类型的问题、泛型的类型安全和代码简洁
 */
public class GenericIntro {
    public static void main(String[] args) {
        System.out.println("===== 类型安全对比 =====\n");

        // 1. 无泛型(原始类型):类型不安全
        System.out.println("--- 无泛型(危险)---");
        List rawList = new ArrayList();
        rawList.add("hello");
        rawList.add(123);     // 可以混入任何类型!
        // String s = (String) rawList.get(1);  // 运行时 ClassCastException!
        System.out.println("  rawList: " + rawList + " (混合类型,取值需强转)");

        // 2. 有泛型:编译期类型安全
        System.out.println("\n--- 有泛型(安全)---");
        List<String> safeList = new ArrayList<>();
        safeList.add("hello");
        safeList.add("world");
        // safeList.add(123);  // 编译错误!类型不匹配
        String s = safeList.get(0);  // 无需强转
        System.out.println("  safeList: " + safeList + " (类型统一,无需强转)");

        // 3. 泛型带来的好处总结
        System.out.println("\n--- 泛型的好处 ---");
        System.out.println("  1. 编译期类型检查,避免 ClassCastException");
        System.out.println("  2. 自动类型转换,无需手动强转");
        System.out.println("  3. 代码复用:一套代码适用于多种类型");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 14.1.4 泛型入门训练题

训练1:创建一个不使用泛型的 ArrayList,先添加一个 String,再添加一个 Integer,然后尝试将第二个元素取出并转为 String。观察运行时会发生什么错误。再用泛型 ArrayList<String> 重写,观察编译时的区别。

训练2:Java 的泛型和 C++ 的模板都能实现"一份代码支持多种类型",但实现方式完全不同。请简要描述两者的区别,并说明 Java 为什么选择了类型擦除方案。

思考:如果 Java 泛型不使用类型擦除,而是像 C# 那样在虚拟机层面真正支持泛型(称为"具化泛型",reified generics),Java 的集合框架会有哪些不同?

# 14.2 泛型类

# 14.2.1 泛型类定义

public class Box<T> {
    private T content;

    public void put(T item) {
        this.content = item;
    }

    public T get() {
        return content;
    }
}

// 使用
Box<String> stringBox = new Box<>();
stringBox.put("Hello");
String s = stringBox.get();  // 不需要强制转换

Box<Integer> intBox = new Box<>();
intBox.put(123);
int num = intBox.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 14.2.2 多类型参数

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}

Pair<String, Integer> pair = new Pair<>("age", 25);
System.out.println(pair.getKey() + ": " + pair.getValue());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

常见的类型参数命名规范:

参数名 含义
T Type(类型)
E Element(元素,常用于集合)
K Key(键)
V Value(值)
N Number(数字)
R Result(返回值)
S, U 第二、第三类型参数

泛型类的继承:

// 泛型类可以继承
public class NamedBox<T> extends Box<T> {
    private String name;
    
    public NamedBox(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return name + ": " + get();
    }
}

// 继承时可以指定具体类型
public class StringBox extends Box<String> {
    // 此时 StringBox 不再是泛型类
    // put(String item) 和 String get() 已经确定了类型
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 14.2.3 综合案例:通用键值对容器

本案例实现一个泛型类,演示多类型参数和泛型嵌套。

import java.util.*;

/**
 * 通用键值对容器 —— 泛型类综合案例
 * 知识点:泛型类定义、多类型参数<K,V>、泛型嵌套
 */
class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }

    @Override
    public String toString() {
        return key + " = " + value;
    }
}

class PairList<K, V> {
    private List<Pair<K, V>> pairs = new ArrayList<>();  // 泛型嵌套

    public void add(K key, V value) {
        pairs.add(new Pair<>(key, value));
    }

    public V findByKey(K key) {
        for (Pair<K, V> p : pairs) {
            if (p.getKey().equals(key)) return p.getValue();
        }
        return null;
    }

    public void printAll() {
        for (Pair<K, V> p : pairs) {
            System.out.println("  " + p);
        }
    }
}

public class GenericClassDemo {
    public static void main(String[] args) {
        System.out.println("===== 通用键值对容器 =====\n");

        // 字符串→整数
        PairList<String, Integer> scores = new PairList<>();
        scores.add("张三", 92);
        scores.add("李四", 85);
        scores.add("王五", 78);
        System.out.println("--- 成绩表 ---");
        scores.printAll();
        System.out.println("  查找张三: " + scores.findByKey("张三"));

        // 整数→字符串
        PairList<Integer, String> errorCodes = new PairList<>();
        errorCodes.add(200, "OK");
        errorCodes.add(404, "Not Found");
        errorCodes.add(500, "Server Error");
        System.out.println("\n--- 错误码 ---");
        errorCodes.printAll();
        System.out.println("  查找404: " + errorCodes.findByKey(404));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 14.2.4 泛型类训练题

训练1:实现一个泛型类 Triple<A, B, C>,存储三个不同类型的值,并提供 getFirst()、getSecond()、getThird() 方法。

训练2:实现一个泛型类 Range<T extends Comparable<T>>,表示一个范围 [min, max],提供 contains(T value) 方法判断值是否在范围内。

思考:Box<String> 是 Box<Object> 的子类吗?为什么?这和数组 String[] 是 Object[] 的子类的行为有什么不同?

# 14.3 泛型方法

# 14.3.1 泛型方法定义

public class ArrayUtils {
    // 泛型方法:<T> 放在返回类型前面
    public static <T> void printArray(T[] arr) {
        for (T item : arr) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 14.3.2 泛型方法使用

Integer[] nums = {1, 2, 3, 4, 5};
String[] names = {"Alice", "Bob", "Charlie"};

ArrayUtils.printArray(nums);    // 1 2 3 4 5
ArrayUtils.printArray(names);   // Alice Bob Charlie

int maxNum = ArrayUtils.max(10, 20);  // 20
String maxStr = ArrayUtils.max("abc", "xyz");  // xyz
1
2
3
4
5
6
7
8

泛型方法 vs 泛型类的方法:

public class Demo<T> {
    // 这是泛型类的方法,T 来自类的类型参数
    public T getFirst(List<T> list) {
        return list.get(0);
    }
    
    // 这是泛型方法,<E> 是方法自己声明的类型参数,与类的 T 无关
    public <E> E getFirst2(List<E> list) {
        return list.get(0);
    }
    
    // 静态方法不能使用类的类型参数 T(因为 T 需要实例化才确定)
    // public static T wrong(T item) { }  // 编译错误!
    
    // 静态方法必须声明自己的类型参数
    public static <E> E correct(E item) { return item; }  // OK
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 14.3.3 综合案例:泛型工具方法集

本案例实现多个泛型方法,演示类型推断和有界类型参数。

import java.util.*;

/**
 * 泛型工具方法集 —— 泛型方法综合案例
 * 知识点:泛型方法定义、类型推断、有界类型参数<T extends Comparable>
 */
public class GenericUtils {

    // 泛型方法:交换数组元素
    static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 有界类型参数:找最大值
    static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    // 泛型方法:列表转数组
    static <T> Object[] toArray(List<T> list) {
        Object[] arr = new Object[list.size()];
        for (int i = 0; i < list.size(); i++) arr[i] = list.get(i);
        return arr;
    }

    // 泛型方法:打印任意数组
    static <T> void printArray(T[] arr) {
        System.out.print("  [");
        for (int i = 0; i < arr.length; i++) {
            if (i > 0) System.out.print(", ");
            System.out.print(arr[i]);
        }
        System.out.println("]");
    }

    public static void main(String[] args) {
        System.out.println("===== 泛型工具方法集 =====\n");

        // 1. swap
        Integer[] nums = {1, 2, 3, 4, 5};
        System.out.println("--- swap ---");
        System.out.print("  交换前: "); printArray(nums);
        swap(nums, 0, 4);  // 类型自动推断为 Integer
        System.out.print("  交换后: "); printArray(nums);

        // 2. max(有界类型参数)
        System.out.println("\n--- max ---");
        System.out.println("  max(3, 7) = " + max(3, 7));
        System.out.println("  max(\"abc\", \"xyz\") = " + max("abc", "xyz"));

        // 3. printArray 对不同类型
        System.out.println("\n--- printArray ---");
        String[] strs = {"Java", "Python", "Go"};
        printArray(strs);
        Double[] dbls = {3.14, 2.71, 1.41};
        printArray(dbls);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 14.3.4 泛型方法训练题

训练1:实现一个泛型方法 <T> int countOccurrences(T[] array, T target),统计数组中某个元素出现的次数。

训练2:实现一个泛型方法 <T extends Comparable<T>> T[] sort(T[] array),对数组进行排序并返回。

思考:泛型方法中的 <T> 是在什么时候确定具体类型的?是编译时还是运行时?如果 ArrayUtils.max(10, 20) 没有显式指定类型参数,编译器是如何推断出 T 是 Integer 的?

# 14.4 泛型接口

# 14.4.1 泛型接口定义

public interface Repository<T> {
    void save(T entity);
    T findById(int id);
    List<T> findAll();
}

public class AccountRepository implements Repository<Account> {
    @Override
    public void save(Account entity) { /* 实现 */ }

    @Override
    public Account findById(int id) { /* 实现 */ return null; }

    @Override
    public List<Account> findAll() { /* 实现 */ return null; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

泛型接口实现的两种方式:

// 方式一:实现时指定具体类型
public class StringComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return o1.compareTo(o2);
    }
}

// 方式二:实现时保留泛型(实现类也是泛型类)
public class GenericRepository<T> implements Repository<T> {
    private List<T> storage = new ArrayList<>();
    
    @Override
    public void save(T entity) {
        storage.add(entity);
    }
    
    @Override
    public T findById(int id) {
        return id < storage.size() ? storage.get(id) : null;
    }
    
    @Override
    public List<T> findAll() {
        return Collections.unmodifiableList(storage);
    }
}

// 使用
Repository<String> strRepo = new GenericRepository<>();
strRepo.save("Hello");
Repository<Integer> intRepo = new GenericRepository<>();
intRepo.save(42);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 14.4.2 综合案例:泛型接口实现

本案例通过不同方式实现泛型接口,演示指定类型和保留泛型参数。

import java.util.*;

/**
 * 泛型接口实现 —— 泛型接口综合案例
 * 知识点:泛型接口定义、实现时指定类型、实现时保留泛型
 */
interface Repository<T> {
    void save(T entity);
    T findById(int id);
    List<T> findAll();
}

// 实现时指定具体类型
class StringRepository implements Repository<String> {
    private Map<Integer, String> store = new HashMap<>();
    private int nextId = 1;

    @Override
    public void save(String entity) {
        store.put(nextId++, entity);
    }

    @Override
    public String findById(int id) {
        return store.get(id);
    }

    @Override
    public List<String> findAll() {
        return new ArrayList<>(store.values());
    }
}

// 实现时保留泛型参数(更通用)
class MemoryRepository<T> implements Repository<T> {
    private Map<Integer, T> store = new HashMap<>();
    private int nextId = 1;

    @Override
    public void save(T entity) {
        store.put(nextId++, entity);
    }

    @Override
    public T findById(int id) {
        return store.get(id);
    }

    @Override
    public List<T> findAll() {
        return new ArrayList<>(store.values());
    }
}

public class GenericInterfaceDemo {
    public static void main(String[] args) {
        System.out.println("===== 泛型接口实现 =====\n");

        // 指定类型的实现
        StringRepository strRepo = new StringRepository();
        strRepo.save("Hello");
        strRepo.save("World");
        System.out.println("StringRepository: " + strRepo.findAll());

        // 保留泛型的实现(可用于任何类型)
        MemoryRepository<Integer> intRepo = new MemoryRepository<>();
        intRepo.save(100);
        intRepo.save(200);
        System.out.println("MemoryRepository<Integer>: " + intRepo.findAll());

        MemoryRepository<Double> dblRepo = new MemoryRepository<>();
        dblRepo.save(3.14);
        System.out.println("MemoryRepository<Double>: " + dblRepo.findAll());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 14.4.3 泛型接口训练题

训练1:定义一个泛型接口 Converter<F, T>,包含方法 T convert(F from)。然后实现 StringToIntConverter(将 String 转 Integer)和 IntToStringConverter(将 Integer 转 String)。

训练2:定义一个泛型接口 Validator<T>,包含方法 boolean validate(T item) 和 String getErrorMessage()。实现 AgeValidator(验证年龄在0-150之间)和 EmailValidator(验证邮箱包含@符号)。

思考:Java 的 Comparable<T> 和 Comparator<T> 都是泛型接口,为什么设计成泛型的而不是直接用 Object?

# 14.5 通配符

# 14.5.1 无界通配符

// ? 表示任意类型
public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printList(List.of(1, 2, 3));
printList(List.of("a", "b", "c"));
1
2
3
4
5
6
7
8
9

# 14.5.2 上界通配符

// ? extends Number:Number 或 Number 的子类
public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number num : list) {
        total += num.doubleValue();
    }
    return total;
}

sum(List.of(1, 2, 3));        // List<Integer>,OK
sum(List.of(1.5, 2.5));       // List<Double>,OK
// sum(List.of("a", "b"));    // List<String>,编译错误
1
2
3
4
5
6
7
8
9
10
11
12

# 14.5.3 下界通配符

// ? super Integer:Integer 或 Integer 的父类
public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List<Number> numList = new ArrayList<>();
addNumbers(numList);  // OK
List<Object> objList = new ArrayList<>();
addNumbers(objList);  // OK
1
2
3
4
5
6
7
8
9
10
11

PECS 原则:Producer Extends, Consumer Super。读取数据用 extends,写入数据用 super。

# 14.5.4 PECS原则深度解析

疑惑:为什么 List<? extends Number> 不能添加元素,而 List<? super Integer> 不能安全读取?

这是泛型通配符最让人困惑的地方。看起来 ? extends Number 表示"Number 的子类",那为什么不能往里面添加 Integer(它就是 Number 的子类啊)?

答疑:关键在于编译器不知道 ? 具体是什么类型。List<? extends Number> 可能是 List<Integer>、List<Double> 或 List<BigDecimal>。如果实际类型是 List<Double>,你往里面添加 Integer 就破坏了类型安全。编译器无法在编译时确定具体类型,所以干脆禁止所有写入操作(除了 null)。

论证:

// 假设允许写入,会发生什么?
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);

List<? extends Number> numList = intList;  // 合法的向上转型
// numList.add(3.14);  // 如果允许,实际添加到了 List<Integer> 中!
// Integer i = intList.get(2);  // 取出 3.14 赋给 Integer,类型不安全!

// 但读取是安全的:
Number n = numList.get(0);  // OK,取出的一定是 Number 的子类
1
2
3
4
5
6
7
8
9
10
11
// 反过来,List<? super Integer> 的情况
List<Number> numList2 = new ArrayList<>();
List<? super Integer> superList = numList2;

superList.add(1);   // OK,Integer 一定可以放入 Integer 的父类容器
superList.add(2);   // OK

// 但读取时不知道具体类型:
// Integer i = superList.get(0);  // 编译错误!可能是 Number 或 Object
Object o = superList.get(0);     // 只能用 Object 接收
1
2
3
4
5
6
7
8
9
10

结果展示:PECS 原则的口诀总结:

  • Producer Extends:当你只需要从集合中读取数据时,使用 ? extends T。集合是数据的"生产者"。
  • Consumer Super:当你只需要往集合中写入数据时,使用 ? super T。集合是数据的"消费者"。
  • 既读又写:不用通配符,直接用 T。

JDK 中的经典应用:Collections.copy(List<? super T> dest, List<? extends T> src)——从 src(生产者)读取数据,写入 dest(消费者)。

# 14.5.5 综合案例:通配符使用场景

本案例通过实际场景演示三种通配符和 PECS 原则。

import java.util.*;

/**
 * 通配符使用场景 —— 通配符综合案例
 * 知识点:?、? extends T、? super T、PECS原则
 */
public class WildcardDemo {

    // ? extends Number:只读(Producer)
    static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) {
            total += n.doubleValue();
        }
        return total;
    }

    // ? super Integer:只写(Consumer)
    static void fillNumbers(List<? super Integer> list, int count) {
        for (int i = 1; i <= count; i++) {
            list.add(i);  // 可以写入 Integer
        }
    }

    // ?:只做通用操作(不读不写具体类型)
    static void printList(List<?> list) {
        System.out.print("  [");
        for (int i = 0; i < list.size(); i++) {
            if (i > 0) System.out.print(", ");
            System.out.print(list.get(i));
        }
        System.out.println("]");
    }

    public static void main(String[] args) {
        System.out.println("===== 通配符使用场景 =====\n");

        // 1. ? extends(上界):从集合中读取
        System.out.println("--- ? extends Number(Producer/读取)---");
        List<Integer> ints = List.of(1, 2, 3);
        List<Double> dbls = List.of(1.5, 2.5);
        System.out.println("  sum(Integer): " + sum(ints));
        System.out.println("  sum(Double) : " + sum(dbls));

        // 2. ? super(下界):往集合中写入
        System.out.println("\n--- ? super Integer(Consumer/写入)---");
        List<Number> numList = new ArrayList<>();
        fillNumbers(numList, 3);  // List<Number> 可以接受 Integer
        System.out.print("  fillNumbers → "); printList(numList);

        // 3. ?(无界):通用操作
        System.out.println("\n--- ?(无界通配符)---");
        printList(ints);
        printList(dbls);
        printList(List.of("A", "B", "C"));

        // 4. PECS 总结
        System.out.println("\n--- PECS原则 ---");
        System.out.println("  Producer Extends: 读取用 ? extends T");
        System.out.println("  Consumer Super  : 写入用 ? super T");
        System.out.println("  既读又写        : 不用通配符,用精确类型 T");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 14.5.6 通配符训练题

训练1:编写方法 double sum(List<? extends Number> list),计算列表中所有数字的和。然后分别用 List<Integer>、List<Double>、List<Long> 测试。

训练2:编写方法 void fillList(List<? super Integer> list, int count),向列表中填充 1 到 count 的整数。然后分别用 List<Integer>、List<Number>、List<Object> 测试。

训练3:以下代码哪些行能编译通过?请逐行分析:

List<? extends Number> list1 = new ArrayList<Integer>();
list1.add(1);          // ① 能否通过?
Number n = list1.get(0);  // ② 能否通过?

List<? super Integer> list2 = new ArrayList<Number>();
list2.add(1);          // ③ 能否通过?
Integer i = list2.get(0);  // ④ 能否通过?
1
2
3
4
5
6
7

思考:List<?> 和 List<Object> 有什么区别?List<?> 和原始类型 List 又有什么区别?

# 14.6 类型擦除

# 14.6.1 擦除机制

类型擦除的底层原理:Java 泛型是通过编译器而非 JVM 实现的。编译时,编译器进行类型检查确保类型安全,然后在生成字节码时擦除所有泛型信息:无界类型参数 <T> 替换为 Object,有界类型参数 <T extends Number> 替换为 Number,并在需要的地方自动插入类型转换指令(checkcast)。这种设计的原因是向后兼容——JDK 5 引入泛型时,需要保证泛型代码与 JDK 1.4 及之前的非泛型代码(原始类型 Raw Type)二进制兼容。虽然运行时泛型信息丢失了,但通过 Signature 属性仍可在 .class 文件中保留泛型签名(供反射使用,如 Field.getGenericType())。

Java 泛型在编译后会被擦除,运行时没有泛型信息:

// 编译前
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);

// 编译后(类型擦除)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0);  // 编译器自动插入强制转换
1
2
3
4
5
6
7
8
9

# 14.6.2 擦除的影响

// 1. 不能用基本类型作为类型参数
// List<int> list;  // 编译错误,必须用 List<Integer>

// 2. 不能创建泛型数组
// T[] arr = new T[10];  // 编译错误

// 3. 运行时泛型类型相同
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());  // true(都是 ArrayList)
1
2
3
4
5
6
7
8
9
10

对比 C++:C++ 模板不存在类型擦除,每种类型会生成独立的代码(代码膨胀),但运行时性能更高。Java 通过类型擦除避免了代码膨胀,但牺牲了一些运行时类型信息。

# 14.6.3 绕过类型擦除的方式

虽然泛型信息在运行时被擦除了,但 Java 提供了几种方式来获取泛型信息:

方式一:通过 Signature 属性获取(反射)

// 类的字段、方法参数中的泛型信息保存在 .class 文件的 Signature 属性中
public class GenericInfo {
    private List<String> names;  // 泛型信息保留在字段签名中
    
    public static void main(String[] args) throws Exception {
        Field field = GenericInfo.class.getDeclaredField("names");
        Type type = field.getGenericType();
        if (type instanceof ParameterizedType pt) {
            System.out.println("原始类型:" + pt.getRawType());  // List
            System.out.println("类型参数:" + pt.getActualTypeArguments()[0]);  // String
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

方式二:通过匿名子类获取(TypeToken 技巧)

// Gson 和 Jackson 框架常用的技巧
// 创建匿名子类时,父类的泛型信息会保留在子类的 Signature 中
abstract class TypeRef<T> {
    Type getType() {
        Type superClass = getClass().getGenericSuperclass();
        return ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }
}

// 使用
TypeRef<List<String>> ref = new TypeRef<List<String>>() {};  // 匿名子类
System.out.println(ref.getType());  // java.util.List<java.lang.String>
1
2
3
4
5
6
7
8
9
10
11
12

方式三:传入 Class 对象

// 最直接的方式:显式传入类型信息
public <T> T create(Class<T> type) throws Exception {
    return type.getDeclaredConstructor().newInstance();
}

String s = create(String.class);  // 传入 Class 对象作为类型标记
1
2
3
4
5
6

# 14.6.4 综合案例:类型擦除探测器

本案例通过代码实验揭示类型擦除的行为和影响。

import java.util.*;
import java.lang.reflect.*;

/**
 * 类型擦除探测器 —— 类型擦除综合案例
 * 知识点:擦除机制、运行时无泛型、反射绕过擦除
 */
public class ErasureDetector {
    public static void main(String[] args) throws Exception {
        System.out.println("===== 类型擦除探测器 =====\n");

        // 1. 运行时类型相同
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println("--- 运行时类型比较 ---");
        System.out.println("  List<String>.class == List<Integer>.class : "
                + (strList.getClass() == intList.getClass()));  // true!
        System.out.println("  实际类型: " + strList.getClass().getName());

        // 2. 无法用 instanceof 判断泛型类型
        System.out.println("\n--- instanceof 限制 ---");
        System.out.println("  strList instanceof List : " + (strList instanceof List));
        // strList instanceof List<String>  // 编译错误!
        System.out.println("  无法写: strList instanceof List<String>");

        // 3. 反射绕过类型擦除
        System.out.println("\n--- 反射绕过擦除 ---");
        List<Integer> safeList = new ArrayList<>();
        safeList.add(1);

        // 通过反射往 List<Integer> 里加 String
        Method addMethod = safeList.getClass().getMethod("add", Object.class);
        addMethod.invoke(safeList, "非法字符串");

        System.out.println("  List<Integer> 内容: " + safeList);
        System.out.println("  类型擦除后运行时无类型检查!");

        // 4. 擦除后的方法签名
        System.out.println("\n--- 擦除后的方法签名 ---");
        for (Method m : safeList.getClass().getMethods()) {
            if (m.getName().equals("add") && m.getParameterCount() == 1) {
                System.out.println("  add 参数类型: " + m.getParameterTypes()[0].getName());
            }
        }
        System.out.println("  擦除后:add(Object) 而非 add(Integer)");

        // 5. 总结
        System.out.println("\n--- 类型擦除总结 ---");
        System.out.println("  编译时: 泛型提供类型安全检查");
        System.out.println("  运行时: 泛型信息被擦除,全部变为 Object");
        System.out.println("  影响  : 不能 new T()、不能 instanceof T、不能创建泛型数组");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 14.6.5 类型擦除训练题

训练1:编写以下代码,观察输出结果:

ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());
1
2
3
4

然后思考:既然运行时泛型信息被擦除了,为什么 IDE 和编译器还能检查泛型类型?

训练2:尝试通过反射"绕过"泛型约束,往 ArrayList<String> 中添加一个 Integer:

ArrayList<String> list = new ArrayList<>();
// 用反射获取 add 方法并调用...
1
2

这说明了什么?

思考:Java 的 Project Valhalla 正在推进"具化泛型"(Reified Generics),让泛型信息在运行时保留。如果成功实现,哪些现有的泛型限制会被消除?

上次更新: 2026/06/10, 11:13:41
线程和锁
注解和反射

← 线程和锁 注解和反射→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式