泛型
泛型用于把类型作为参数传给类、接口或方法。它让代码在保持复用性的同时获得编译期类型检查。
没有泛型时,集合只能保存 Object:
List list = new ArrayList();
list.add("Java");
String value = (String) list.get(0);
有泛型后:
List<String> list = new ArrayList<>();
list.add("Java");
String value = list.get(0);
泛型的核心收益:
- 编译期发现类型错误。
- 减少强制类型转换。
- 提升 API 表达能力。
- 让容器、工具方法、框架扩展点更通用。
# 1. 泛型类
泛型类把类型参数放在类名后。
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
使用:
Box<String> box = new Box<>();
box.set("hello");
String value = box.get();
T 是类型参数,创建对象时由具体类型替换。
常见类型参数命名:
| 名称 | 含义 |
|---|---|
T | Type |
E | Element |
K | Key |
V | Value |
R | Result |
? | 未知类型 |
# 2. 泛型接口
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
实现时指定类型:
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) {
return null;
}
@Override
public void save(User entity) {
}
}
也可以让实现类继续保持泛型:
public class MemoryRepository<T, ID> implements Repository<T, ID> {
public T findById(ID id) {
return null;
}
public void save(T entity) {
}
}
# 3. 泛型方法
泛型方法把类型参数放在返回值前。
public static <T> T first(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
调用:
String first = first(List.of("A", "B"));
Integer number = first(List.of(1, 2));
多个类型参数:
public static <K, V> Map<K, V> mapOf(K key, V value) {
Map<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
泛型方法适合把类型关系绑定在一次方法调用中。
# 4. 类型擦除
Java 泛型主要在编译期生效,运行期会进行类型擦除。
List<String> names = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(names.getClass() == numbers.getClass()); // true
运行时它们都是 ArrayList。
擦除过程简化理解:
源码
List<String> names
│ 编译期类型检查
▼
字节码
List names
│ 必要位置插入强转
▼
运行期
普通 ArrayList 对象
示例:
String value = names.get(0);
编译后大致类似:
String value = (String) names.get(0);
类型擦除带来的限制:
- 不能直接创建泛型数组。
- 运行期不能用
instanceof List<String>。 - 不能获取
T.class。 - 泛型重载可能冲突。
# 5. 原始类型 Raw Type
不指定泛型参数就是原始类型。
List list = new ArrayList();
list.add("hello");
list.add(123);
问题:
List<String> names = list;
String name = names.get(1); // ClassCastException
原始类型绕过了编译期检查,把错误推迟到运行期。
开发中除非兼容老 API,否则不要使用原始类型。
# 6. 泛型通配符
# 6.1 ?
? 表示未知类型。
public void print(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
List<?> 可以接收任意元素类型的列表,但不能随意添加具体元素。
List<?> list = new ArrayList<String>();
// list.add("x"); // 编译失败
list.add(null); // 可以
因为编译器不知道真实元素类型是什么。
# 6.2 上界通配符 ? extends
? extends Number 表示某种 Number 子类型。
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}
可以传入:
sum(List.of(1, 2, 3));
sum(List.of(1.1, 2.2));
但不能添加具体数字:
List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(1); // 编译失败
原因:
List<? extends Number>
可能是 List<Integer>
可能是 List<Double>
可能是 List<BigDecimal>
编译器不知道实际是哪一种,所以不允许写入具体 Number 子类。
它适合读取,因此常叫 Producer。
# 6.3 下界通配符 ? super
? super Integer 表示某种 Integer 父类型。
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
可以传入:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(integers);
addNumbers(numbers);
addNumbers(objects);
读取时只能安全地当作 Object:
Object value = list.get(0);
它适合写入,因此常叫 Consumer。
# 7. PECS 原则
PECS:Producer Extends, Consumer Super。
| 场景 | 写法 | 理由 |
|---|---|---|
| 从集合中读取 T | ? extends T | 生产 T |
| 向集合中写入 T | ? super T | 消费 T |
示例:
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
结构:
src: List<? extends T> 负责生产元素
│
▼
T
│
▼
dest: List<? super T> 负责接收元素
这是理解 Collections.copy 等 API 的关键。
# 8. 泛型边界
可以限制类型参数上界:
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() {
return value.doubleValue();
}
}
多个边界:
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
规则:如果有类边界,类必须放在第一个,接口放后面。
<T extends SomeClass & InterfaceA & InterfaceB>
# 9. 泛型数组限制
不能直接创建泛型数组:
// T[] array = new T[10]; // 编译失败
也不能创建具体泛型数组:
// List<String>[] lists = new List<String>[10]; // 编译失败
原因是数组在运行期知道元素类型,而泛型在运行期被擦除,两者类型机制不匹配。
数组是协变且运行期检查:
Object[] array = new String[1];
array[0] = 123; // ArrayStoreException
泛型是不变且编译期检查:
List<Object> objects = new ArrayList<String>(); // 编译失败
# 10. 泛型不变性
即使 Integer 是 Number 的子类,List<Integer> 也不是 List<Number> 的子类。
List<Integer> integers = new ArrayList<>();
// List<Number> numbers = integers; // 编译失败
如果允许,会出现问题:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;
numbers.add(1.5);
Integer value = integers.get(0); // 类型污染
所以 Java 泛型默认不变。需要灵活接收时使用通配符。
# 11. 桥接方法
类型擦除可能导致编译器生成桥接方法,保证多态仍然正确。
class Parent<T> {
T get() {
return null;
}
}
class Child extends Parent<String> {
@Override
String get() {
return "x";
}
}
擦除后父类方法类似 Object get(),子类方法是 String get()。编译器会生成桥接方法:
Child
├─ String get() 真实方法
└─ Object get() bridge,内部调用 String get()
这也是反射查看方法时可能看到 bridge 方法的原因。
# 12. 泛型与反射
虽然泛型会擦除,但类、字段、方法签名中的泛型信息有些会保留在元数据中。
Field field = UserService.class.getDeclaredField("users");
Type type = field.getGenericType();
框架经常利用这些信息解析:
- Controller 参数类型。
- JSON 序列化目标类型。
- ORM 实体字段类型。
- 依赖注入候选类型。
但局部变量的泛型信息通常不能在运行期直接获取。
# Tips 快问快答
Q:泛型解决什么问题? A:让类型错误尽量在编译期暴露,并减少强制类型转换。
Q:Java 泛型是真泛型还是伪泛型? A:Java 泛型基于类型擦除,运行期大多看不到具体类型参数。
Q:为什么不能 new T()?
A:类型擦除后运行期不知道 T 的具体类,也不知道该调用哪个构造器。
Q:为什么不能 new T[10]?
A:数组需要运行期元素类型,泛型类型参数运行期被擦除。
Q:List<Integer> 是 List<Number> 的子类吗?
A:不是。泛型默认不变,避免类型污染。
Q:什么时候用 ? extends T?
A:主要从集合读取数据时使用。
Q:什么时候用 ? super T?
A:主要向集合写入数据时使用。
Q:PECS 怎么记?
A:生产者用 extends,消费者用 super。
Q:Raw Type 有什么问题? A:绕过泛型检查,可能把类型错误推迟到运行期。
Q:为什么反射里会看到 bridge 方法? A:编译器为了在类型擦除后保持泛型多态语义,会生成桥接方法。