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. 多类型参数

方法可处理多个泛型类型。修改原方法支持 TG 两种类型:

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);
}

即使 HouseBuilding 的子类,也不能用 List<House> 调用此方法。

✅ 解决方案:使用有界通配符

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

现在方法可接受 Building 及其所有子类。这称为上界通配符Building 是上界。

也可使用下界通配符,用 super 关键字声明:

<? super T>  // 表示 T 及其所有父类

5. 类型擦除

泛型在编译期保证类型安全。为避免运行时开销,编译器会对泛型进行类型擦除

  1. 移除所有类型参数
  2. 替换为边界或 Object(无界时)
  3. 编译后的字节码只包含普通类/接口/方法
  4. 在编译期插入必要的类型转换

示例:

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

Listadd 方法:

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 ValhallaJEP 218

7. 总结

Java 泛型是语言的重要增强,它让开发更简单、更少出错。泛型在编译期强制类型检查,最重要的是——它允许实现通用算法且不会带来运行时开销。

本文配套源码可在 GitHub 获取。


原始标题:The Basics of Java Generics | Baeldung