概述

泛型是在编译器工作,编译完成后泛型信息会被擦除(Type Erasure),运行时 JVM 根本不知道你用了什么泛型。泛型主要是能在编译器校验类型是否安全,并且能强制消除转换,并能提高代码的复用率。

泛型的作用

泛型解决了三个核心问题:

  • 类型安全:编译期就能发现类型错误,而不是等到运行时才爆 ClassCastException
  • 消除强制转换:不再需要手动 (String) list.get(0),编译器自动插入转换代码
  • 代码复用:一套逻辑适配多种数据类型,避免为每种类型写重复代码
// 没有泛型:不安全,需要手动强转
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);  // 如果里面混入了 Integer,运行时直接炸

// 有泛型:编译期就能发现错误
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);  // 无需强转,类型安全
// list.add(123);         // 编译报错!

泛型接口、泛型类、泛型方法、泛型字段的定义

泛型类

在类名后声明类型参数,整个类都可以使用:

public class Box<T> {
    private T value;  // 泛型字段

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

    public T get() {
        return value;
    }
}

// 使用
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // 无需强转

// 多类型参数
public class Pair<K, V> {
    private K key;
    private V value;
}

泛型接口

在接口名后声明类型参数:

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

// 实现方式一:指定具体类型
public class UserRepository implements Repository<User> {
    @Override
    public void save(User entity) { /* ... */ }
    @Override
    public User findById(int id) { /* ... */ }
}

// 实现方式二:延续泛型
public class GenericRepository<T> implements Repository<T> {
    @Override
    public void save(T entity) { /* ... */ }
    @Override
    public T findById(int id) { /* ... */ }
}

泛型方法

类型参数声明在返回值之前,独立于类的泛型参数:

public class Util {
    // 静态泛型方法(不能引用类的泛型参数)
    public static <T> T getFirst(List<T> list) {
        return list.get(0);
    }

    // 有界泛型方法
    public static <T extends Number> double sum(T[] numbers) {
        double total = 0;
        for (T num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }
}

// 调用时编译器自动推断类型
Integer first = Util.getFirst(List.of(1, 2, 3));

泛型字段

泛型字段定义在泛型类内部,使用类声明的类型参数:

public class Container<T> {
    private T data;           // 泛型字段
    private List<T> items;    // 泛型集合字段

    // ❌ 编译错误:静态成员不能使用类的泛型参数
    // private static T staticData;

    // ✅ 静态方法需要自己声明泛型参数
    public static <U> U getDefault(U defaultValue) {
        return defaultValue;
    }
}

通配符

通配符解决的是泛型灵活性问题——List<Integer> 不是 List<Number> 的子类(泛型不支持协变),通配符让 API 更灵活。

通配符

含义

能读

能写

适用场景

<?>

任意类型

Object

只能 null

只遍历,不关心类型

<? extends T>

T 或其子类

T

只能 null

生产者(只读)

<? super T>

T 或其父类

Object

T 及其子类

消费者(只写)

PECS 原则

PECS 原则是 Java 泛型通配符使用的核心指导方针,全称是 Producer Extends, Consumer Super,意思是:如果这个集合/容器是"生产者"(只读),用 ? extends T;如果是"消费者"(只写),用 ? super T。

? extends T(上界通配符)

表示"某个 T 或 T 的子类",但具体是哪个子类,编译器不知道。编译器只知道它是 T 的某个子类型,但不知道具体是哪个,写入时无法保证类型安全

List<? extends Number> list = new ArrayList<Integer>();

这里 list 可能是 List<Integer>、List<Double>、List<Float>……编译器无法确定具体是哪个。

? super T(下界通配符)

表示"某个 T 或 T 的父类",但具体是哪个父类,编译器不知道。编译器只知道它是 T 的某个父类,但不知道具体是哪个,可以写入,但是可以读,但是是Object,作用不大。

List<? super Integer> list = new ArrayList<Number>();

List<? super Integer> list = new ArrayList<Number>();

这里 list 可能是 List<Integer>、List<Number>、List<Object>……编译器无法确定具体是哪个。

PECS 原则总结

角色

含义

通配符

能做什么

Producer(生产者)

从中读取数据

? extends T

能 get()

,不能 add()

Consumer(消费者)

往里写入数据

? super T

能 add()

,get()

只能得到 Object

一句话记忆:读用 extends,写用 super。又取又放 ,不用通配符,用具体泛型。

?和T 的区别

  • T(类型参数):定义类/方法时声明的确定类型占位符。调用时确定具体类型后,方法内所有 T 都代表同一个类型。
  • ?(通配符):使用时表示未知类型。编译器不知道具体是什么,只知范围。
类型一致性

T 能确保多个参数类型一致,? 无法保证:

// T 保证 dest 和 src 元素类型相同
public <T extends Number> void copy(List<T> dest, List<T> src) { ... }

// ? 无法保证两个 List 类型相同(dest 可能是 List<Integer>,src 可能是 List<Double>)
public void copy(List<? extends Number> dest, List<? extends Number> src) { ... }
读写能力

T 可读可写,? 受 PECS 原则限制:但是T不能使用下界通配符

// T 可读可写
public <T extends Number> void process(List<T> list) {
    T item = list.get(0);   // 读 
    list.add(item);         // 写 
}

// ? extends 只能读不能写
public void process(List<? extends Number> list) {
    Number item = list.get(0);  // 读 
    // list.add(new Integer(1)); // 写 编译报错!编译器不知具体是 Integer 还是 Double
}
声明变量

T 可声明局部变量,? 不行:

public <T extends Number> void method1(List<T> list) {
    T temp = list.get(0);  // ✅
}

public void method2(List<? extends Number> list) {
    // ? temp = list.get(0);  // ❌ 无法用 ? 声明变量
    Number temp = list.get(0);  // 只能退化为上界类型
}
限定能力

能力

T

?

上界 extends

✅ <T extends Number>

✅ <? extends Number>

下界 super

❌ 不支持

✅ <? super Number>

多重限定

✅ <T extends A & B>

❌ 不支持

PESC(黄金法则)的应用场景

生产者和消费者协同 — 经典 Collections.copy

这是 PECS 最经典的综合应用。src 是生产者(往外读),dest 是消费者(往里写):

public static <T> void copy(List<? super T> dest,    // 消费者:往里写
                            List<? extends T> src) { // 生产者:往外读
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

// 调用:从 Integer 列表复制到 Number 列表
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(dest, src);  // ✅ dest 是 Number,能装下 Integer

更多推荐