泛型基础
概述
泛型(Generics)是 JDK 5 引入的语法特性,允许在定义类、接口、方法时使用类型参数(如 T、E),在使用时再指定具体类型。泛型的主要目的是:在编译期做类型检查,避免把错误留到运行时;同时减少强制类型转换,让代码更安全、更易读。
通俗理解:泛型就像「带占位符的模板」——先写「这里将来会是一种类型 T」,等真正用的时候再告诉编译器「T 是 String」或「T 是 Integer」,编译器会按你指定的类型做检查。
前置建议
已掌握 类与对象、接口,理解多态与向上转型。泛型在 集合 和 Lambda/Stream 中大量使用,建议在学集合前先打好泛型基础。
为什么需要泛型
在没有泛型时,若要让一个类能「装任意类型」,往往使用 Object 引用,取用时再强转,容易在运行时出现 ClassCastException:
// 泛型出现前:用 Object 表示「任意类型」
List list = new ArrayList();
list.add("hello");
list.add(100);
String s = (String) list.get(1); // 运行时 ClassCastException!使用泛型后,类型在编译期就确定,错误在编译阶段就能被发现:
List<String> list = new ArrayList<>();
list.add("hello");
list.add(100); // 编译错误:不能把 Integer 放进 List<String>
String s = list.get(0); // 无需强转,取出的就是 String泛型带来的好处:
- 类型安全:编译期检查类型,减少运行时类型转换错误。
- 消除强转:取出的元素已是目标类型,无需手写
(String)等。 - 代码复用:同一套类/方法可适用于多种类型,而不必为每种类型写重复代码。
泛型类
在类名后加 <类型参数> 即可定义泛型类。类型参数常用单字母:T(Type)、E(Element)、K/V(Key/Value)、N(Number)等。
基本语法
// 类名后声明类型参数 T,在类体内可把 T 当作一种类型使用
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}使用示例
// 指定 T 为 String
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get(); // 无需强转
// 指定 T 为 Integer
Box<Integer> intBox = new Box<>();
intBox.set(100);
Integer n = intBox.get();
// 若不指定泛型(原始类型),会失去类型检查,不推荐
Box rawBox = new Box();
rawBox.set("hello");
// 取用时需强转,且容易出错注意
使用泛型类时尽量始终带上类型实参(如 Box<String>),避免使用原始类型(raw type)Box,否则会退回「Object + 强转」的旧写法,失去泛型的类型安全。
多个类型参数
类可以声明多个类型参数,用逗号分隔,例如表示键值对的类:
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// 使用
Pair<String, Integer> pair = new Pair<>("age", 25);
String k = pair.getKey(); // "age"
Integer v = pair.getValue(); // 25泛型接口
接口也可以带类型参数,定义与泛型类类似;实现类在实现接口时指定具体类型,或保持为泛型类。
定义泛型接口
// 可比较的「盒子」,比较逻辑由实现类决定,但比较的是同类型 T
public interface ComparableBox<T> {
int compareTo(T other);
}实现方式一:实现时指定具体类型
public class StringBox implements ComparableBox<String> {
private String value;
public StringBox(String value) {
this.value = value;
}
@Override
public int compareTo(String other) {
return value.compareTo(other);
}
}实现方式二:实现类仍是泛型类
public class Box<T> implements ComparableBox<T> {
private T value;
// 假设 T 本身实现了 Comparable<T>,这里仅作演示
private java.util.Comparator<T> comparator;
public Box(T value, java.util.Comparator<T> comparator) {
this.value = value;
this.comparator = comparator;
}
@Override
public int compareTo(T other) {
return comparator.compare(value, other);
}
}标准库中的 List、Set、Map 等都是泛型接口,使用时会写成 List<String>、Map<Integer, String> 等。
泛型方法
泛型方法是在方法上声明类型参数,而不是在类上。格式为:在修饰符与返回类型之间写 <类型参数>,方法参数或返回类型中即可使用该类型参数。
基本语法
// 在 static 与返回类型 void 之间声明类型参数 T
public static <T> void printArray(T[] array) {
for (T elem : array) {
System.out.println(elem);
}
}
// 调用时编译器根据实参推断 T
printArray(new String[]{"a", "b"}); // T 推断为 String
printArray(new Integer[]{1, 2, 3}); // T 推断为 Integer带返回值的泛型方法
// 从列表中取第一个元素,列表元素类型为 T,返回类型也是 T
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
String first = getFirst(Arrays.asList("a", "b", "c")); // "a"
Integer num = getFirst(Arrays.asList(1, 2, 3)); // 1泛型方法与泛型类的区别
- 泛型类:类型参数写在类名后,整个类的实例都与该类型绑定(如
Box<String>)。 - 泛型方法:类型参数写在方法上,仅该方法使用该类型;可以是静态方法,且每次调用可以推断出不同的类型。
public class Utils {
// 静态泛型方法:类不需要是泛型类
public static <T> boolean isEmpty(List<T> list) {
return list == null || list.isEmpty();
}
}提示
当方法逻辑「与具体类型无关、只与结构有关」时,用泛型方法即可,不必把整个类都写成泛型类。
类型擦除
Java 的泛型在编译后并不会保留在字节码里,而是通过 类型擦除(type erasure)被擦掉:编译器把类型参数替换成其上界(未指定上界时即为 Object),并在需要时插入强制转换。因此,运行时 JVM 看不到泛型信息,只能看到「原始类型」。
擦除后的效果
// 源码
public class Box<T> {
private T value;
public T get() { return value; }
}
// 擦除后(逻辑上等价,实际字节码中 T 变为 Object)
// class Box {
// private Object value;
// public Object get() { return value; }
// }因此:
Box<String>和Box<Integer>在运行时都只是Box,无法在运行时区分。- 不能写出
if (obj instanceof Box<String>)这样的判断(可写obj instanceof Box,但拿不到String)。 - 不能写
new T()或new T[],因为运行时没有 T 的信息。
类型擦除带来的限制
| 限制 | 说明 |
|---|---|
| 不能使用基本类型作为类型实参 | 只能写 List<Integer>,不能写 List<int>,需用包装类 |
不能 new T() / new T[] | 运行时不知道 T 是谁,无法实例化 |
| 静态成员不能引用类型参数 | 静态属于类,而 T 属于实例,故 static T field 非法 |
| 不能创建参数化类型的数组 | 如 new List<String>[10] 不合法,可写 new List<?>[10] 或用集合 |
示例:不合法写法
public class Example<T> {
// 错误:静态成员不能使用类型参数 T
// private static T staticField;
public void bad() {
// 错误:不能 new T()
// T obj = new T();
// 错误:不能 new T[]
// T[] array = new T[10];
}
}
// 错误:不能创建 List<String> 的数组
// List<String>[] arr = new List<String>[10];版本说明
若需在运行时保留类型信息,可配合反射在运行时读取泛型签名(如 ParameterizedType),但日常业务代码中更多是接受类型擦除、在编译期利用泛型做类型检查即可。
注意事项
注意
- 重写带泛型的方法时,子类方法上的类型参数或签名要与父类/接口一致,避免变成重载或编译错误。
- 泛型只存在于编译期,运行时无法根据「是不是
List<String>」做分支,只能根据「是不是List」判断。
提示
- 类型参数命名建议统一:
T表示一般类型,E表示集合元素,K/V表示键/值,N表示数字,便于团队阅读。 - 在学完 通配符 后,可以更灵活地声明方法参数(如
List<? extends Number>),既保证类型安全又提高 API 的适用性。
相关链接
- 泛型通配符 —
?、extends、super的用法与边界 - 接口 — 泛型接口与多态
- 集合概述 — 集合框架中的泛型(List、Set、Map)
- Oracle Java 教程 - 泛型