什么是泛型?为什么要使用它?
结论:泛型(generics)把"类型"变成参数,让同一个类或方法在编译期获得类型检查与自动类型转换,避免 Object + 强转带来的 ClassCastException。它的三重收益是:类型安全(错误提前到编译期)、消除强制转换(代码更干净)、代码复用(一套逻辑适配多种类型)。Java 的泛型是编译期特性,通过**类型擦除(type erasure)**实现,运行期不存在 List<String> 这个类型。
1. 没有泛型时有多痛
List list = new ArrayList(); // 原始类型,元素都是 Object
list.add("订单A");
list.add(100);
String s = (String) list.get(1); // 运行期抛 ClassCastException
问题有三:编译期发现不了类型错误、每取一个元素都要强转、API 无法表达"这个集合装的是什么"。泛型把这三件事一次性解决。
| 维度 | 无泛型 | 有泛型 |
|---|---|---|
| 类型错误发现时机 | 运行期(可能是生产环境) | 编译期 |
| 取元素 | 必须强转 | 自动插入 checkcast,源码无感 |
| 类型表达能力 | 注释里写"这里放 String" | List<String> 写进签名 |
| 代码复用 | 每种类型写一份,或全用 Object | 一份代码 + 类型参数 |
| 可读性 | 差,靠约定 | 好,靠编译器 |
2. 泛型的三种形态
泛型类: class Box<T> { private T value; }
泛型接口: interface Mapper<T, R> { R map(T t); }
泛型方法: static <T> T pick(List<T> list)
通配符: List<? extends Number>(读)、List<? super Integer>(写)
3. 代码:从原始类型到泛型
import java.util.ArrayList;
import java.util.List;
public class GenericDemo {
/** 泛型方法 + 上界:T 必须可比较 */
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("empty");
T best = list.get(0);
for (T t : list) {
if (t.compareTo(best) > 0) best = t;
}
return best;
}
/** 泛型类 */
static class Box<T> {
private T value;
Box(T value) { this.value = value; }
T get() { return value; }
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(); // 钻石操作符,类型推断
names.add("Tom");
// names.add(1); // 编译期报错,而不是运行期
String first = names.get(0); // 不需要强转
Box<Integer> box = new Box<>(42);
int v = box.get(); // 自动拆箱
System.out.println(max(List.of(3, 9, 5))); // 9
}
}
4. 擦除:为什么运行期看不到泛型
源码: List<String> list = new ArrayList<>();
list.add("a");
String s = list.get(0);
编译后(擦除后):
List list = new ArrayList();
list.add("a");
String s = (String) list.get(0); ← 编译器插入 checkcast
泛型参数 T 的上界默认是 Object;
<T extends Number> 擦除后变成 Number。
由此产生四条限制:
| 限制 | 原因 | 绕法 |
|---|---|---|
不能 new T() | 运行期不知道 T 是什么 | 传入 Class<T> 或 Supplier<T> |
不能 new T[] | 数组需要具体组件类型 | (T[]) new Object[n] + @SuppressWarnings,或用 List<T> |
不能 instanceof List<String> | 擦除后都是 List | 用 instanceof List<?> 判断,再逐个检查元素 |
| 静态成员不能用 T | 静态成员属于类,不随实例参数化 | 改成泛型方法 |
5. 常见追问
| 追问 | 回答要点 |
|---|---|
| 泛型可以用基本类型吗? | 不能,List<int> 非法,必须用包装类 Integer(装箱开销) |
List<Object> 和 List<String> 是父子关系吗? | 不是,泛型不协变(invariant),这是为了类型安全 |
? extends 和 ? super 怎么选? | PECS 原则:生产者用 extends(只读),消费者用 super(只写) |
| 泛型重载会怎样? | f(List<String>) 与 f(List<Integer>) 擦除后签名相同,编译报错 |
| 桥接方法是什么? | 子类重写泛型方法时,编译器生成擦除签名的桥方法做转发(详见擦除专题) |
为什么 Arrays.asList 返回的 List 不能 add? | 返回的是固定大小的内部实现,泛型只保证类型,不保证可变性 |
小结
泛型的价值是把类型错误从运行期提前到编译期,并让 API 自解释。它是编译期语法糖,底层靠擦除实现,所以有"不能 new T、不能泛型数组、基本类型不行"这些限制。回答时把"为什么用"和"擦除的代价"一起讲,才算完整。