1. 概述
JDK 5.0 引入了 Java 泛型,核心目标是减少 bug 并为类型增加一层抽象。
本教程将快速介绍 Java 泛型的基础知识、设计初衷以及如何提升代码质量。
2. 为什么需要泛型
假设我们要创建一个存储 Integer
的列表:
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
但编译器会报错!它不知道返回的数据类型是什么。必须显式转换:
Integer i = (Integer) list.iterator.next();
❌ 问题在于:没有机制能保证列表返回的是 Integer
。这个列表可以存储任何对象,编译器只能确定返回的是 Object
。
这种强制转换很烦人——我们明明知道列表里是 Integer
。而且:
- 代码变得冗余
- 如果显式转换写错,会导致运行时类型错误
✅ 更好的方式是让开发者声明特定类型,由编译器保证类型安全。这就是泛型的核心思想。
修改第一行代码:
List<Integer> list = new LinkedList<>();
通过添加钻石操作符 <>
指定类型,我们限定了列表只能存储 Integer
。编译器会在编译期强制检查类型。
在小程序中这可能显得微不足道,但在大型项目中,泛型能显著提升代码健壮性和可读性。
3. 泛型方法
泛型方法通过单个方法声明就能处理不同类型的参数,编译器会保证类型安全。其特点包括:
- 方法返回类型前用
<T>
声明类型参数 - 类型参数可以限定边界(后文详述)
- 方法签名中可声明多个类型参数(用逗号分隔)
- 方法体与普通方法无异
示例:将数组转换为列表的泛型方法
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
⚠️ 即使方法返回 void
,<T>
声明也必不可少。
3.1. 多类型参数
方法可处理多个泛型类型。修改原方法支持 T
和 G
两种类型:
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
传入转换函数,将 T
类型数组转为 G
类型列表。示例:将 Integer
转为 String
:
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
Oracle 建议用大写字母表示泛型类型,并选择描述性强的字母。Java 集合中:
T
表示类型(Type)K
表示键(Key)V
表示值(Value)
3.2. 有界泛型
类型参数可以限定边界(即限制可接受的类型)。例如:
- 上界:接受指定类型及其子类
- 下界:接受指定类型及其父类
声明上界使用 extends
关键字:
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
这里的
extends
表示:
- 若边界是类 →
T
继承该类- 若边界是接口 →
T
实现该接口
3.3. 多重边界
类型参数可同时有多个上界:
<T extends Number & Comparable>
⚠️ 如果边界中包含类(如 Number
),必须放在第一位,否则编译报错。
4. 泛型中的通配符
通配符用 ?
表示,指代未知类型。在泛型中特别有用,常作为参数类型。
但先记住一个关键点:**Object
是所有 Java 类的父类,但 Collection<Object>
不是任何集合的父类!**
例如:
List<Object>
不是List<String>
的父类- 将
List<Object>
赋值给List<String>
会导致编译错误
这是为了防止向同一集合添加不同类型的数据。此规则适用于任何类型及其子类型的集合。
看这个例子:
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
即使 House
是 Building
的子类,也不能用 List<House>
调用此方法。
✅ 解决方案:使用有界通配符
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
现在方法可接受 Building
及其所有子类。这称为上界通配符,Building
是上界。
也可使用下界通配符,用 super
关键字声明:
<? super T> // 表示 T 及其所有父类
5. 类型擦除
泛型在编译期保证类型安全。为避免运行时开销,编译器会对泛型进行类型擦除:
- 移除所有类型参数
- 替换为边界或
Object
(无界时) - 编译后的字节码只包含普通类/接口/方法
- 在编译期插入必要的类型转换
示例:
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
类型擦除后(无界类型 T
替换为 Object
):
// 仅为示意
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
// 实际编译结果
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
有界类型示例:
public <T extends Building> void genericMethod(T t) {
...
}
编译后变为:
public void genericMethod(Building t) {
...
}
6. 泛型与基本数据类型
⚠️ Java 泛型限制:类型参数不能是基本类型
以下代码无法编译:
List<int> list = new ArrayList<>();
list.add(17);
原因在于:泛型是编译期特性,类型参数会被擦除,所有泛型类型最终都实现为 Object
。
看 List
的 add
方法:
List<Integer> list = new ArrayList<>();
list.add(17);
方法签名:
boolean add(E e);
编译后变为:
boolean add(Object e);
因此类型参数必须能转换为 Object
。基本类型不继承 Object
,所以不能用作类型参数。
✅ 解决方案:使用包装类 + 自动装箱/拆箱
Integer a = 17; // 自动装箱
int b = a; // 自动拆箱
创建整数列表:
List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);
编译后等效于:
List list = new ArrayList<>();
list.add(Integer.valueOf(17)); // 装箱
int first = ((Integer) list.get(0)).intValue(); // 拆箱
未来 Java 版本可能支持基本类型泛型(参考 Project Valhalla 和 JEP 218)
7. 总结
Java 泛型是语言的重要增强,它让开发更简单、更少出错。泛型在编译期强制类型检查,最重要的是——它允许实现通用算法且不会带来运行时开销。
本文配套源码可在 GitHub 获取。