十七、泛型
泛型=类型标签 + 编译器检查
我要把员工按部门分组,没有泛型只能写:
List list = new ArrayList();
list.add(new Employee());
Employee e = (Employee) list.get(0);
每次都强转,还可能转型错;有了泛型之后:
List<Employee> list = new ArrayList<>();
list.add(new Employee());
Employee e = list.get(0);
这样更安全,也不需要重复手动强制转换。
1. 泛型是什么
泛型就是在类、接口、方法中声明类型参数,用来约束容器里存放的数据类型。
例如:
List<String> names = new ArrayList<>();
List<Integer> ages = new ArrayList<>();
这里的 String、Integer 是类型参数,编译器会在编译阶段检查是否传入了正确的类型。
2. 编译期类型检查
泛型最大的好处,是在编译期就发现错误,而不是等到运行时才崩掉。
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(123); // 编译错误
如果没有泛型,程序可能在运行时出现 ClassCastException:
List list = new ArrayList();
list.add("Alice");
Integer i = (Integer) list.get(0); // 运行时抛异常
泛型让错误提前暴露,减少了很多隐藏 bug。
3. 运行时类型擦除
Java 的泛型不是在运行时保留真实类型参数的。它在字节码中会被“擦除”,也就是编译器在生成字节码时,去掉泛型信息。
例如:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
这两个变量在运行时实际都对应同一个原始类型:
List.class
也就是说:
List<String> == List<Integer> == List
在字节码层面,都是 java.util.List。
所以,List<String> 和 List<Integer> 在运行时是同一个 Class,原因是:
- 泛型只存在于源代码和编译期
- 编译器负责检查类型安全
- 运行时真正存在的是擦除后的原始类型
4. 为什么是“同一个 Class”
因为 JVM 不保存泛型参数信息,JVM 看到的只是:
class ArrayList implements List
而不是:
class ArrayList<String>
这也解释了为什么反射里拿到的泛型信息有限,很多时候只能看到 List.class,而看不到 String 或 Integer。
5. 但为什么还需要泛型
虽然运行时没保留泛型参数,但泛型仍然非常有用,因为它:
- 提供编译期检查
- 避免大量强制类型转换
- 提高代码可读性
- 降低运行时错误概率
例如:
List<String> names = new ArrayList<>();
String first = names.get(0);
这里 get(0) 直接返回 String,无需再写:
String first = (String) names.get(0);
6. 一个直观的理解
可以把泛型理解为:
- 编译期:给容器“贴上标签”
- 运行时:标签会被擦掉,只留原始类型
也就是说,泛型更像是“编译器的约束机制”,而不是 JVM 运行时的真实类型系统。
7. 总结
泛型的核心是:
- 在编译期检查类型
- 在运行时类型擦除
- 让
List<String>和List<Integer>在 JVM 中都变成同一个原始类型List
所以,答案是:
List<String> 和 List<Integer> 是同一个 Class,原因是泛型是在编译器处理的,而不是 JVM 运行时保留的。
这是 Java 泛型设计中的关键特性,也正是它既安全又简洁的原因之一。
