泛型基础
理解泛型类、泛型方法、类型擦除机制
泛型基础
泛型(Generics)是 Java 5 引入的重大特性。它让你在编译期就能发现类型错误,而不是等到运行时才抛出 ClassCastException。
学完本章你将: 理解为什么需要泛型、定义泛型类和泛型方法、认识类型擦除机制。
为什么需要泛型
在没有泛型的时代,集合里可以放任意类型的对象,取出来必须强制转型。一旦放错类型,编译不报错,运行直接崩溃。
// ❌ Java 5 之前的写法 —— 类型不安全
List list = new ArrayList();
list.add("hello");
list.add(123); // 完全合法!编译不报错
String s = (String) list.get(0); // 被迫强转
String s2 = (String) list.get(1); // 💥 运行时 ClassCastException!
泛型解决了这个问题: 在声明集合时就约定好"只能放 String",编译器帮你把关。
// ✅ 使用泛型 —— 编译期检查
List<String> strList = new ArrayList<>();
strList.add("hello"); // ✅ 正确类型
// strList.add(123); // ❌ 编译就报错,根本跑不起来
String s = strList.get(0); // 无需强转,类型已确定
💡 核心思想: 泛型把类型检查从"运行时"提前到"编译时"。越早发现错误,代价越低。
泛型类
泛型类就是在类名后加 <T>,T 是个类型参数(占位符),真正使用时才指定具体类型。
可以把 Box<T> 想象成"一个能装任意类型东西的盒子"。定义时还不知道装什么——T 就是那个未知类型。使用时 Box<String> 就锁定了——这个盒子只能装字符串。
// 定义泛型类:<T> 是类型参数,可以叫任何名字(如 T、E、K、V)
public class Box<T> {
private T value; // T 类型的成员变量
public void set(T value) { this.value = value; } // 参数是 T
public T get() { return value; } // 返回值是 T
public boolean isEmpty() { return value == null; }
}
// 使用时指定具体类型
Box<String> strBox = new Box<>(); // T 被替换为 String
strBox.set("Hello"); // 只能传 String
String val = strBox.get(); // 取出就是 String,不用强转
Box<Integer> intBox = new Box<>(); // T 被替换为 Integer
intBox.set(42);
约定俗成的类型参数名:
| 字母 | 含义 | 常见场景 |
|---|---|---|
| `T` | Type | 通用类型 |
| `E` | Element | 集合元素 |
| `K` | Key | Map 的键 |
| `V` | Value | Map 的值 |
| `R` | Return | 返回值类型 |
泛型方法
泛型方法可以独立于泛型类存在。即使一个普通类,也可以有泛型方法。方法名前的 <T> 声明了"这个方法使用类型参数 T",然后参数和返回值就能用 T。
和泛型类的区别: 泛型类的 T 作用域是整个类,泛型方法的 T 作用域只是这一个方法。两者互不影响。
public class Utils {
// <T> 声明类型参数 T 作为返回类型 List<T> 使用类型参数
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) return null;
return list.get(0);
}
// 交换数组中两个位置 —— 能用于任意类型数组
public static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 调用泛型方法时,编译器自动推断 T 的类型
Integer[] nums = {1, 2, 3};
Utils.swap(nums, 0, 2); // 自动推断 T = Integer
System.out.println(Arrays.toString(nums)); // [3, 2, 1]
String[] words = {"A", "B", "C"};
Utils.swap(words, 0, 1); // 自动推断 T = String
⚠️ 注意: 调用时通常不需要显式写
<Integer>,编译器能根据你传入的参数类型自动推断。只有推断失败时才需要手动指定。
类型擦除
这是泛型最容易被误解的地方。泛型只存在于编译期。 编译完成后,所有 <T> 会被擦除,替换为 Object(或上界类型)。
// 写的代码
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 编译后变成(伪代码)
List strList = new ArrayList(); // <String> 被擦除了
List intList = new ArrayList(); // <Integer> 也被擦除了
所以运行时两者的 Class 完全一样:
System.out.println(strList.getClass() == intList.getClass()); // true!
类型擦除带来的限制:
// ❌ 不能 new 泛型数组 —— 擦除后 JVM 不知道 T 是什么类型
// T[] arr = new T[10]; // 编译错误
// List<String>[] arr = new List<String>[10]; // 编译错误
// ❌ 不能直接用 instanceof 检查泛型
// if (obj instanceof List<String>) {} // 编译错误(擦除后只剩 List)
// ❌ 不能重载泛型方法 —— 擦除后签名相同
// void method(List<String> list) {}
// void method(List<Integer> list) {} // 编译错误!擦除后签名一致
💡 为什么 Java 要擦除? 为了向后兼容。Java 5 引入泛型时,必须保证旧的 Java 1.4 字节码仍能在新 JVM 上运行。如果泛型在运行时保留,所有旧代码都得重写。