第23章 泛型——类型的保险箱

第二十三章 泛型——类型的保险箱

“给你一个盒子,你不知道里面装的是什么——这叫Object。 给你一个贴了标签的盒子,你一眼就知道里面装的是什么——这叫泛型。”

泛型,是 Java 给类型系统买的一份保险。


23.1 为什么需要泛型?

故事的起源:没有泛型的日子

在 Java 5 之前,集合(Collection)是这样用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 模拟 Java 5 之前的代码
public class OldStyleBox {
    private Object value;

    public OldStyleBox(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }
}

// 使用时
OldStyleBox box = new OldStyleBox("Hello");
// 取出来用,得强制转型
String s = (String) box.getValue();

这段代码看起来没什么问题,直到有一天:

1
2
3
4
OldStyleBox box = new OldStyleBox(123);
// 我以为取出来是 String,实际情况是...
String s = (String) box.getValue(); // 编译通过,运行时炸了!
// 抛出 ClassCastException: java.lang.Integer cannot be cast to java.lang.String

💥 ClassCastException — 编译时笑嘻嘻,运行时哭唧唧。

问题出在哪?编译器在强制转型那一刻完全信任了你,而你自己骗了自己。类型检查形同虚设。

泛型的救赎

Java 5 引入泛型,革命开始了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 泛型版本
public class GenericBox<T> {
    private T value;

    public GenericBox(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

// 使用时
GenericBox<String> box = new GenericBox<>("Hello");
String s = box.getValue(); // 不需要转型!直接就是 String!

如果你敢这么写:

1
2
GenericBox<Integer> box = new GenericBox<>(123);
String s = box.getValue(); // 编译错误!Integer 不能赋值给 String

编译器在编译时就把你拦下来了,根本不给 ClassCastException 上场的机会。

泛型的三大好处

好处说明
类型安全编译器检查类型,消灭 ClassCastException
消除强制转型getValue() 直接返回目标类型,不用 cast
代码复用一套泛型类/方法,可以支持所有类型

简单来说:泛型把类型检查从"事后诸葛亮"变成了"事前保安"


23.2 泛型类

什么是泛型类?

泛型类就是**带有类型参数(Type Parameter)**的普通类。类型参数用尖括号 <T> 声明,放在类名后面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 泛型类的基本语法
 * T 是类型参数(Type Parameter),名字可以随便起,
 * 但约定俗成有:T(Type)、K/V(Key/Value)、E(Element)、R(Return)
 */
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; }

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

使用泛型类时,在尖括号里指定具体的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Main {
    public static void main(String[] args) {
        // K=String, V=Integer
        Pair<String, Integer> ageMap = new Pair<>("Alice", 30);
        System.out.println(ageMap); // Pair{Alice=30}

        // K=String, V=String
        Pair<String, String> config = new Pair<>("host", "localhost");
        System.out.println(config); // Pair{host=localhost}

        // 编译器自动推断类型(钻石语法)
        Pair<String, Double> rate = new Pair<>("USD/EUR", 0.92);
        System.out.println(rate); // Pair{USD/EUR=0.92}
    }
}

💡 钻石语法(Diamond Syntax):Java 7 开始,new Pair<>("...", 123)<> 可以空着,编译器会自动推断。

多个类型参数

泛型类可以有多个类型参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Triple<A, B, C> {
    private A first;
    private B second;
    private C third;

    public Triple(A first, B second, C third) {
        this.first = first;
        this.second = second;
        this.third = third;
    }

    public A getFirst() { return first; }
    public B getSecond() { return second; }
    public C getThird() { return third; }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ", " + third + ")";
    }
}

// 使用
Triple<String, Integer, Boolean> flags = new Triple<>("debug", 1, true);
System.out.println(flags); // (debug, 1, true)

泛型类的继承

泛型类可以继承泛型类,也可以被非泛型类继承:

 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
// 父类
public class GenericParent<T> {
    protected T data;
    public T getData() { return data; }
}

// 子类保留泛型
class StringGenericChild<T> extends GenericParent<T> {
    @Override
    public String toString() {
        return "StringGenericChild{data=" + data + "}";
    }
}

// 子类具体化泛型
class StringSpecificChild extends GenericParent<String> {
    // data 的类型被固定为 String
}

// 子类新增类型参数
class ExtendedGenericChild<T, V> extends GenericParent<T> {
    private V extra;
    public ExtendedGenericChild(T data, V extra) {
        this.data = data;
        this.extra = extra;
    }
}

23.3 泛型接口

基本语法

泛型接口和泛型类的语法几乎一样,只是关键字换成了 interface

 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
/**
 * 泛型接口:Comparable 接口被重新设计为泛型版本
 * 实现了泛型化的 Comparable,可以指定比较对象的类型
 */
public interface Comparable<T> {
    int compareTo(T other);
}

/**
 * 泛型容器接口
 */
public interface Container<T> {
    void add(T item);
    T get(int index);
    int size();
    boolean isEmpty();
}

/**
 * 实现泛型接口时,可以选择:
 * 1. 保持泛型
 * 2. 具体化类型
 */
public class StringList implements Container<String> {
    private String[] items = new String[100];
    private int count = 0;

    @Override
    public void add(String item) {
        if (count < items.length) {
            items[count++] = item;
        }
    }

    @Override
    public String get(int index) {
        if (index >= 0 && index < count) {
            return items[index];
        }
        throw new IndexOutOfBoundsException("索引: " + index);
    }

    @Override
    public int size() { return count; }

    @Override
    public boolean isEmpty() { return count == 0; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        StringList list = new StringList();
        list.add("Java");
        list.add("Python");
        list.add("Go");

        System.out.println("第一个语言: " + list.get(0)); // Java
        System.out.println("总数: " + list.size()); // 3

        // 尝试添加非 String?编译器直接报错
        // list.add(123); // 编译错误!
    }
}

泛型接口的多种实现方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 泛型接口
public interface Transformer<T, R> {
    R transform(T input);
}

// 实现方式一:保持泛型
class ArrayToList<T> implements Transformer<T, java.util.List<T>> {
    @Override
    public java.util.List<T> transform(T input) {
        return java.util.List.of(input);
    }
}

// 实现方式二:具体化类型
class StringToUpperCase implements Transformer<String, String> {
    @Override
    public String transform(String input) {
        return input.toUpperCase();
    }
}

23.4 泛型方法

什么是泛型方法?

泛型方法是指在方法返回类型前面声明了类型参数的方法。它可以是静态的,也可以是非静态的,甚至可以在普通类里定义。

 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
public class Algorithm {

    /**
     * 泛型方法:交换数组中两个位置的元素
     * <T> 是方法类型参数声明,必须在方法返回类型之前
     */
    public static <T> void swap(T[] array, int i, int j) {
        if (i < 0 || j < 0 || i >= array.length || j >= array.length) {
            throw new IllegalArgumentException("索引越界");
        }
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    /**
     * 泛型方法:找出数组中的最大值
     * Comparable<T> 是类型参数约束,下一节会详细讲
     */
    public static <T extends Comparable<T>> T findMax(T[] array) {
        if (array == null || array.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
        return max;
    }

    /**
     * 泛型方法:把两个对象装进 Pair
     */
    public static <K, V> Pair<K, V> makePair(K key, V value) {
        return new Pair<>(key, value);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Main {
    public static void main(String[] args) {
        // 测试 swap
        String[] languages = {"Java", "Python", "Go", "Rust"};
        Algorithm.swap(languages, 0, 2);
        System.out.println("交换后: " + java.util.Arrays.toString(languages));
        // [Go, Python, Java, Rust]

        // 测试 findMax
        Integer[] numbers = {3, 9, 1, 4, 7, 2, 8};
        Integer max = Algorithm.findMax(numbers);
        System.out.println("最大值: " + max); // 9

        // 测试 makePair
        Pair<String, Integer> pair = Algorithm.makePair("score", 100);
        System.out.println(pair); // Pair{score=100}
    }
}

泛型方法和泛型类的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class MyClass<T> {
    // 这是泛型类的方法,不是泛型方法!
    // T 来自类的类型参数
    public T doSomething(T input) {
        return input;
    }

    // 这才是真正的泛型方法!方法自己声明了 <V>
    public <V> V convert(T input, Class<V> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

🎯 判断技巧:看方法的返回类型前面有没有 <Something>。有,就是泛型方法;没有,就是用类的类型参数的普通方法。


23.5 类型参数约束

上界 bounded wildcard —— extends

有时候,你需要限制泛型允许的类型范围。比如"只要是数字就行"或"只要实现了某个接口就行"。这时就用 extends 关键字设置上界(Upper Bound)

 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
/**
 * 计算所有数字类型的通用计算器
 * <T extends Number> 表示 T 必须是 Number 或 Number 的子类
 * 这样 T 就一定有 intValue()、doubleValue() 等方法
 */
public class NumberCalculator<T extends Number> {
    private T[] numbers;

    @SafeVarargs
    public NumberCalculator(T... numbers) {
        this.numbers = numbers;
    }

    /**
     * 计算总和(自动转换为 double)
     */
    public double sum() {
        double total = 0.0;
        for (T n : numbers) {
            total += n.doubleValue(); // 安全的,因为 T 一定是 Number 的子类
        }
        return total;
    }

    /**
     * 计算平均值
     */
    public double average() {
        return numbers.length == 0 ? 0 : sum() / numbers.length;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        NumberCalculator<Integer> intCalc = new NumberCalculator<>(1, 2, 3, 4, 5);
        System.out.println("整数总和: " + intCalc.sum());       // 15.0
        System.out.println("整数平均: " + intCalc.average());    // 3.0

        NumberCalculator<Double> doubleCalc = new NumberCalculator<>(1.5, 2.5, 3.0);
        System.out.println("Double总和: " + doubleCalc.sum());  // 7.0
        System.out.println("Double平均: " + doubleCalc.average()); // 2.333...

        // NumberCalculator<String> strCalc = new NumberCalculator<>("a"); // 编译错误!
        // String 不是 Number 的子类,不满足 <T extends Number> 约束
    }
}

多重约束

Java 也支持多重约束,用 & 连接:

1
2
3
4
5
6
7
/**
 * T 必须同时实现 Serializable 和 Comparable 接口
 * 这在设计框架类时非常有用
 */
public static <T extends Serializable & Comparable<T>> void process(T item) {
    // T 一定可以序列化和比较
}

⚠️ 注意:多重约束中,如果有类参与约束,必须放在第一位。因为 Java 不允许多继承,但接口可以多继承,所以用 & 连接时,类只能出现一次且必须在最前。

下界约束 —— super(在通配符中详细讲)

下界约束用在通配符中 <T super SomeType>,留待 23.6 节。


23.6 泛型通配符

通配符 ? —— 不知道是什么类型

通配符 ? 表示"我不知道是什么类型"(或者"我不在乎是什么类型")。它有三种形态:

形态名称含义用途
<?>无界通配符不知道什么类型只读操作
<? extends T>上界通配符不知道具体类型,但知道是 T 或 T 的子类生产者(读取数据)
<? super T>下界通配符不知道具体类型,但知道是 T 或 T 的父类消费者(写入数据)

PECS 原则:Producer Extends, Consumer Super

这是 Java 泛型中最著名的原则之一。

 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
import java.util.ArrayList;
import java.util.List;

/**
 * PECS: Producer Extends, Consumer Super
 *
 * 如果你的泛型参数是用来**生产(读取)**数据的,用 extends
 * 如果你的泛型参数是用来**消费(写入)**数据的,用 super
 */
public class WildcardDemo {

    // 生产者:读取数据,只关心能读出什么
    public static double sumOfAll(List<? extends Number> numbers) {
        double total = 0.0;
        for (Number n : numbers) {
            total += n.doubleValue(); // 可以安全读取
        }
        // numbers.add(1); // 编译错误!不能写入(不知道具体类型)
        return total;
    }

    // 消费者:写入数据
    public static void addNumbers(List<? super Integer> list) {
        // 可以安全写入 Integer 或其子类
        list.add(1);
        list.add(2);
        list.add(3);
        // Integer i = list.get(0); // 编译错误!只能当 Object 取出来
    }

    public static void copy(List<? extends Object> src, List<? super Object> dest) {
        for (Object item : src) {
            dest.add(item); // 写入 OK
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Main {
    public static void main(String[] args) {
        // extends 用法
        List<Integer> integers = List.of(1, 2, 3);
        List<Double> doubles = List.of(1.1, 2.2, 3.3);

        System.out.println("整数和: " + WildcardDemo.sumOfAll(integers)); // 6.0
        System.out.println("Double和: " + WildcardDemo.sumOfAll(doubles)); // 6.6

        // super 用法
        List<Number> numbers = new ArrayList<>();
        List<Object> objects = new ArrayList<>();
        WildcardDemo.addNumbers(numbers);
        WildcardDemo.addNumbers(objects);
        System.out.println("Numbers: " + numbers); // [1, 2, 3]
        System.out.println("Objects: " + objects); // [1, 2, 3]
    }
}

<?> 无界通配符

当你既不需要读也不需要写具体类型时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * 打印列表中的元素(只读)
 * List<?> 可以接受 List<String>、List<Integer> 等任何类型
 */
public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
    // list.add("something"); // 编译错误!不能写入
}

为什么 List<Object>List<?> 不一样?

这是一个经典面试题:

1
2
List<Object> objectList = new ArrayList<String>(); // 编译错误!
List<?>  wildcardList  = new ArrayList<String>(); // 编译通过!

List<Object> 看起来更"宽",为什么反而不允许?

因为如果允许 List<Object> = new ArrayList<String>(),那么就可以往里面 add(new Integer(123)),但底层其实是 ArrayList<String>,运行时就会乱套。泛型的设计初衷就是类型安全,所以这种"看似合理"的赋值被禁止了。

List<?> 则明确表示"我什么都不知道",所以只能读(返回 Object),不能写(编译器也不知道你写进去的是什么)。


23.7 类型擦除

泛型的幕后真相

Java 的泛型是伪泛型——它在编译后就把泛型信息擦掉了。这就是所谓的类型擦除(Type Erasure)

为什么要擦除?因为 Java 诞生于 1995 年,泛型是 2004 年才加进来的。为了保持向后兼容(让旧代码还能跑),Java 选择了"编译时检查,运行时忽略"的方案。

类型擦除的过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 源代码
public class ErasureBox<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

编译后(字节码层面等价于):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 类型擦除后的样子(伪代码,实际上 .class 里看不到 T)
public class ErasureBox {
    private Object value;  // T 被替换为 Object

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) { // T 被替换为 Object
        this.value = value;
    }
}

对于有上界的泛型,擦除规则如下:

泛型声明擦除后
<T>Object
<T extends Number>Number
<T extends Comparable & Serializable>Comparable

桥接方法(Bridge Methods)

类型擦除会导致一个有趣的现象——桥接方法。当你继承一个泛型类或实现一个泛型接口时,编译器会自动生成桥接方法来保持多态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 源代码
public interface Comparable<String> {
    int compareTo(String other);
}

public class Person implements Comparable<Person> {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
}

编译后,Person 类里会有两个 compareTo 方法:

1
2
3
4
5
6
7
8
9
// 编译器自动生成的桥接方法
public int compareTo(Object other) {
    return compareTo((Person) other); // 调用我们的 compareTo(Person)
}

// 我们自己写的
public int compareTo(Person other) {
    return this.name.compareTo(other.name);
}

这就是为什么你调 person.compareTo(anotherPerson) 能正常工作的原因——编译器在背后偷偷帮你做了适配。

类型擦除的影响

因为类型信息被擦除了,有些事情在泛型代码里是做不了的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ErasureLimitation<T> {
    // 编译错误!不能 new T()
    public T create() {
        return new T(); // ❌ 无法实例化类型参数
    }

    // 编译错误!不能 new T[]
    public T[] createArray(int size) {
        return new T[size]; // ❌ 无法创建泛型数组
    }

    // 编译错误!不能 instanceof T
    public boolean check(Object obj) {
        return obj instanceof T; // ❌ instanceof 不支持泛型类型
    }

    // 编译错误!不能获取 T 的 Class
    public Class<T> getType() {
        return T.class; // ❌ 不能直接获取
    }
}

绕过类型擦除

虽然类型擦除有限制,但有些技巧可以绕过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class WorkaroundDemo<T> {

    // 绕过1:通过 Class 对象创建实例
    public <T> T createInstance(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }

    // 绕过2:通过反射获取泛型数组的组件类型
    public <T> T[] createArray(int size, Class<T> componentType) {
        @SuppressWarnings("unchecked")
        T[] array = (T[]) java.lang.reflect.Array.newInstance(componentType, size);
        return array;
    }

    // 绕过3:使用泛型标记类
    public static <T> void demo(Class<T> clazz) {
        System.out.println("类型: " + clazz.getName());
    }
}

本章小结

本章我们深入探讨了 Java 泛型(Generics)这座"类型的保险箱":

知识点核心要点
为什么需要泛型泛型将类型检查提前到编译期,消除 ClassCastException,告别强制转型
泛型类<T> 声明类型参数,类名后跟尖括号,如 class Box<T>
泛型接口与泛型类类似,如 interface Transformer<T, R>
泛型方法在返回类型前声明类型参数,如 <T> void method(T arg)
类型参数约束<T extends Number> 限制 T 的上界,多重约束用 & 连接
泛型通配符? extends T(生产者)/ ? super T(消费者)/ ?(无界),遵循 PECS 原则
类型擦除泛型仅在编译期有效,运行时被擦除为 Object(或上界类型),编译器通过桥接方法维护多态

泛型是 Java 类型系统的地基级特性。掌握它,你不仅能写出更安全的代码,还能读懂 Java 集合框架、Stream API、Lombok 等几乎所有现代 Java 库的核心实现。

最后修改 March 30, 2026: 新增 Java 教程 (4da1bd7)