概述
泛型是在编译器工作,编译完成后泛型信息会被擦除(Type Erasure),运行时 JVM 根本不知道你用了什么泛型。泛型主要是能在编译器校验类型是否安全,并且能强制消除转换,并能提高代码的复用率。
泛型的作用
泛型解决了三个核心问题:
- 类型安全 :编译期就能发现类型错误,而不是等到运行时才爆
ClassCastException - 消除强制转换 :不再需要手动
(String) list.get(0),编译器自动插入转换代码 - 代码复用:一套逻辑适配多种数据类型,避免为每种类型写重复代码
bash
// 没有泛型:不安全,需要手动强转
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); // 编译报错!
泛型接口、泛型类、泛型方法、泛型字段的定义
泛型类
在类名后声明类型参数,整个类都可以使用:
bash
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;
}
泛型接口
在接口名后声明类型参数:
bash
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) { /* ... */ }
}
泛型方法
类型参数声明在返回值之前,独立于类的泛型参数:
bash
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));
泛型字段
泛型字段定义在泛型类内部,使用类声明的类型参数:
bash
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 的某个子类型,但不知道具体是哪个,写入时无法保证类型安全
bash
List<? extends Number> list = new ArrayList<Integer>();
这里 list 可能是 List<Integer>、List<Double>、List<Float>......编译器无法确定具体是哪个。
? super T(下界通配符)
表示"某个 T 或 T 的父类",但具体是哪个父类,编译器不知道。编译器只知道它是 T 的某个父类,但不知道具体是哪个,可以写入,但是可以读,但是是Object,作用不大。
bash
List<? super Integer> list = new ArrayList<Number>();
List<? super Integer> list = new ArrayList();
这里 list 可能是 List、List、List......编译器无法确定具体是哪个。
PECS 原则总结
| 角色 | 含义 | 通配符 | 能做什么 | | --- | --- | --- | --- | | Producer(生产者) | 从中读取数据 | ? extends T | 能 get() ,不能 add() | | Consumer(消费者) | 往里写入数据 | ? super T | 能 add() ,get() 只能得到 Object |
一句话记忆 :读用 extends,写用 super。又取又放 ,不用通配符,用具体泛型。
?和T 的区别
**T**(类型参数) :定义类/方法时声明的确定类型占位符 。调用时确定具体类型后,方法内所有T都代表同一个类型。**?**(通配符) :使用时表示未知类型。编译器不知道具体是什么,只知范围。
类型一致性
T 能确保多个参数类型一致,? 无法保证:
bash
// 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不能使用下界通配符
bash
// 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 可声明局部变量,? 不行:
bash
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 是消费者(往里写):
bash
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