--- title: "02-泛型详解" created: 2025-11-26 tags: - Java --- # 泛型详解 ## 核心问题:如何让一个类同时支持多种类型? ### 场景引入 ```java // 需求:设计一个"盒子"类,能存储不同类型的数据 // 问题:如何实现? // 方案1:为每种类型写一个类(❌ 类爆炸) class IntegerBox { private Integer value; } class StringBox { private String value; } class UserBox { private User value; } // ...无穷无尽 // 方案2:使用Object(❌ 类型不安全) class Box { private Object value; public void set(Object value) { this.value = value; } public Object get() { return value; } } Box box = new Box(); box.set("hello"); Integer i = (Integer) box.get(); // 💥 ClassCastException // 方案3:泛型(✅ 类型安全 + 代码复用) class Box { private T value; public void set(T value) { this.value = value; } public T get() { return value; } } Box box = new Box<>(); box.set("hello"); String s = box.get(); // 无需强转,编译期保证类型安全 ``` --- ## 一、为什么需要泛型?(JDK 5之前的痛点) ### 1.1 JDK 5之前的写法 ```java // 没有泛型的集合 List list = new ArrayList(); list.add("hello"); list.add(123); // ✅ 编译通过 list.add(new User()); // ✅ 编译通过 // 取出时必须强转 String s = (String) list.get(0); // ✅ 正常 String s2 = (String) list.get(1); // 💥 运行时 ClassCastException ``` ### 1.2 三大核心问题 | 问题 | 具体表现 | 后果 | | --- | --- | --- | | **类型不安全** | 任何类型都能放入集合 | 运行时才发现类型错误 | | **强制转换繁琐** | 每次取值都需要 `(Type)` | 代码冗长,容易出错 | | **可读性差** | 看不出集合存储的类型 | 维护困难 | ### 1.3 泛型解决方案 ```java List list = new ArrayList<>(); list.add("hello"); // ✅ 正常 list.add(123); // ❌ 编译错误!类型不匹配 list.add(new User()); // ❌ 编译错误! String s = list.get(0); // ✅ 无需强转,编译器保证类型 ``` **泛型的四大好处:** 1. **编译期类型检查** → 错误提前发现 2. **避免强制转换** → 代码更简洁(实际是编译器自动插入了强转) 3. **提升可读性** → `List` 一看就知道存String 4. **代码复用** → 一个类适配所有类型 --- ## 二、泛型的基本使用 ### 2.1 泛型类 ```java public class Box { private T value; public void set(T value) { this.value = value; } public T get() { return value; } } // 使用 Box stringBox = new Box<>(); stringBox.set("hello"); String s = stringBox.get(); Box intBox = new Box<>(); intBox.set(100); Integer i = intBox.get(); ``` ### 2.2 泛型接口 ```java public interface Repository { void save(T entity); T findById(Long id); List findAll(); } // 实现时指定具体类型 public class UserRepository implements Repository { @Override public void save(User entity) { } @Override public User findById(Long id) { return null; } @Override public List findAll() { return null; } } ``` ### 2.3 泛型方法 ```java public class Utils { // 静态泛型方法(必须自己声明类型参数) public static T getFirst(List list) { return list.isEmpty() ? null : list.get(0); } // 多个类型参数 public static Map createMap() { return new HashMap<>(); } // 有界类型参数 public static double sum(List list) { double total = 0; for (T num : list) { total += num.doubleValue(); // Number的方法 } return total; } } ``` --- ## 三、类型擦除(Type Erasure)⭐⭐⭐ ### 3.1 什么是类型擦除? **核心定义:** Java泛型是"伪泛型",编译后所有泛型信息都会被擦除,字节码中只剩下原始类型(Raw Type)。 ```java List stringList = new ArrayList<>(); List intList = new ArrayList<>(); // 运行时,两者的Class完全相同! System.out.println(stringList.getClass() == intList.getClass()); // true!都是 ArrayList.class ``` **反射验证:泛型只在编译期生效** ```java List list = new ArrayList<>(); list.add("hello"); // 通过反射可以绕过泛型检查 Method add = ArrayList.class.getMethod("add", Object.class); add.invoke(list, 100); // ✅ 成功添加Integer! System.out.println(list); // [hello, 100] String s = list.get(1); // 💥 运行时ClassCastException ``` ### 3.2 为什么要类型擦除? **历史原因:向后兼容** ``` Java 1.4(2002年) → 没有泛型,代码已经大量部署 ↓ Java 5(2004年) → 引入泛型,如何兼容旧代码? ↓ 解决方案:编译期检查泛型,运行时擦除泛型信息 ↓ 结果:新代码(有泛型)和旧代码(无泛型)可以无缝共存 ``` **设计思路推演:** 1. ❌ 方案A:JVM层面实现泛型 → 旧字节码无法运行,破坏兼容性 2. ✅ 方案B:编译器层面实现泛型 → 编译后擦除信息,运行时与旧代码一致 **代价与限制:** ```java // 限制1:不能用基本类型 List list; // ❌ 编译错误 // 原因:擦除后变成List,int无法转为Object // 限制2:不能创建泛型数组 T[] array = new T[10]; // ❌ 编译错误 // 原因:擦除后不知道具体类型,无法分配内存 // 限制3:不能用instanceof if (obj instanceof List) { } // ❌ 编译错误 // 原因:运行时List和List是同一个类型 ``` ### 3.3 擦除规则 | 泛型声明 | 擦除后类型 | 说明 | | --- | --- | --- | | `List` | `List` | 原始类型(Raw Type) | | `` | `Object` | 无界类型参数 | | `` | `Number` | 有界类型参数,擦除为上界 | | `>` | `Comparable` | 擦除为第一个上界 | **示例验证:** ```java // 源码 public class Box { private T value; public void set(T value) { this.value = value; } public T get() { return value; } } // 擦除后等价于 public class Box { private Number value; // T → Number public void set(Number value) { this.value = value; } public Number get() { return value; } } ``` ### 3.4 字节码分析:编译器的"魔法" #### ① 为什么get()不需要强转? ```java List list = new ArrayList<>(); list.add("hello"); String str = list.get(0); // 看起来没有强转 ``` **字节码分析(javap -c):** ```java // Java代码 String str = list.get(0); // 字节码 0: aload_1 // 加载list 1: iconst_0 // 加载参数0 2: invokeinterface #2 // 调用get方法,返回Object 7: checkcast #3 // ✅ 强转为String! 10: astore_2 // 存储到str变量 ``` **真相:** 编译器自动插入了 `checkcast` 指令(强制类型转换),所以我们的Java代码中不需要手写。 #### ② 为什么反射能获取泛型信息? **问题:** 类型擦除后,为什么反射还能获取泛型? ```java public class Demo { private List stringList; // 成员变量 public List getIntList() { // 方法返回值 return null; } public void process(List doubleList) { // 方法参数 } } // 反射获取泛型信息 Field field = Demo.class.getDeclaredField("stringList"); Type genericType = field.getGenericType(); System.out.println(genericType); // java.util.List ``` **字节码分析(javap -v):** ```java // .class文件中的签名信息(Signature) Signature: Ljava/util/List; ↑ 泛型信息被保留在这里! ``` **真相:** `.class`文件中保留了签名(Signature)信息,反射通过读取Signature获取泛型类型。 **关键限制:仅3种情况能获取泛型信息** ```java // ✅ 可以获取 1. 成员变量的泛型:Field.getGenericType() 2. 方法参数的泛型:Method.getGenericParameterTypes() 3. 方法返回值的泛型:Method.getGenericReturnType() // ❌ 无法获取 4. 局部变量的泛型 void method() { List localList = new ArrayList<>(); // 局部变量无签名信息 } ``` ### 3.5 桥接方法(Bridge Method) **经典面试题:子类重写泛型方法时发生了什么?** ```java // 父类 public class ArrayList { public boolean add(E e) { ... } } // 子类 public class StringList extends ArrayList { @Override public boolean add(String s) { // 我们重写了add(String) System.out.println("add String: " + s); return super.add(s); } } ``` **问题:** 擦除后父类的add方法参数是`Object`,子类是`String`,这不算重写! **编译器的解决方案:生成桥接方法** ```java // 编译后实际生成了两个方法 // 用户写的 public boolean add(String s) { System.out.println("add String: " + s); return super.add(s); } // 编译器自动生成的桥接方法 public boolean add(Object o) { // 参数是Object,真正重写父类方法 return add((String) o); // 强转后调用用户写的方法 } ``` **验证:** ```java StringList list = new StringList(); Method[] methods = list.getClass().getDeclaredMethods(); for (Method m : methods) { System.out.println(m.getName() + " - " + m.isBridge()); } // 输出: // add - false (用户写的) // add - true (编译器生成的桥接方法) ``` --- ## 四、通配符(?)与上下界 ### 4.1 为什么需要通配符? ```java // 需求:打印任意数字列表 public void printNumbers(List numbers) { for (Number n : numbers) { System.out.println(n); } } List intList = Arrays.asList(1, 2, 3); printNumbers(intList); // ❌ 编译错误! // 原因:List 不是 List 的子类型 ``` **泛型的不可变性(Invariance):** ```java // 即使 Integer 是 Number 的子类 Integer extends Number // 但 List 不是 List 的子类 List ≠ List ``` **解决方案:通配符** ```java public void printNumbers(List numbers) { // ? extends Number 表示"Number或其子类" } printNumbers(Arrays.asList(1, 2, 3)); // ✅ List printNumbers(Arrays.asList(1.1, 2.2)); // ✅ List printNumbers(Arrays.asList(1L, 2L)); // ✅ List ``` ### 4.2 三种通配符对比 | 写法 | 能否读? | 能否写? | 典型场景 | | --- | --- | --- | --- | | `List` | ✅ 读出T | ✅ 写入T | 具体类型,功能最全 | | `List` | ✅ 读出Object | ❌ 只能写null | 未知类型,只读 | | `List` | ✅ 读出T | ❌ 只能写null | 上界,只读(生产者) | | `List` | ⚠️ 读出Object | ✅ 写入T及其子类 | 下界,只写(消费者) | ### 4.3 上界通配符 `? extends T`(生产者) ```java List list = new ArrayList(); // ✅ 可以读取(返回Number) Number n = list.get(0); // ❌ 不能写入(除了null) list.add(100); // ❌ 编译错误 list.add(1.5); // ❌ 编译错误 list.add(null); // ✅ 可以,null是所有类型的子类 ``` **为什么不能写?** ``` 编译器的视角: list可能是 List list也可能是 List list还可能是 List 如果允许 list.add(1.5): - 若实际是 List → 类型污染! - 若实际是 List → 正常 为了类型安全,干脆禁止所有写入操作 ``` ### 4.4 下界通配符 `? super T`(消费者) ```java List list = new ArrayList(); // ✅ 可以写入(Integer及其子类) list.add(100); // ✅ Integer list.add(new Integer(200)); // ✅ Integer // ⚠️ 读取只能用Object接收 Integer i = list.get(0); // ❌ 编译错误 Object o = list.get(0); // ✅ 可以 ``` **为什么能写?** ``` 编译器的视角: list可能是 List list也可能是 List list还可能是 List 但无论哪种情况,Integer的所有父类都能接受Integer 所以写入Integer是类型安全的 ``` --- ## 五、PECS原则(Producer Extends, Consumer Super) **核心口诀:频繁读取用 extends,频繁写入用 super** | 场景 | 推荐写法 | 原因 | | --- | --- | --- | | 只从集合取数据(生产者) | `List` | Producer → Extends | | 只往集合存数据(消费者) | `List` | Consumer → Super | | 既读又写 | `List`(具体类型) | 通配符无法满足 | **经典案例:Collections.copy** ```java public static void copy( List dest, // 消费者:接收数据 List src // 生产者:提供数据 ) { for (int i = 0; i < src.size(); i++) { dest.set(i, src.get(i)); // src生产,dest消费 } } // 使用示例 List dest = new ArrayList<>(); List src = Arrays.asList(1, 2, 3); Collections.copy(dest, src); // ✅ 类型安全 ``` --- ## 六、协变与逆变 ### 6.1 核心概念 **协变(Covariance):** 子类型可以替换父类型 ```java class Animal {} class Dog extends Animal {} // 上界通配符 = 协变 List animals; animals = new ArrayList(); // ✅ 子类型替换父类型 ``` **逆变(Contravariance):** 父类型可以替换子类型 ```java // 下界通配符 = 逆变 List dogs; dogs = new ArrayList(); // ✅ 父类型替换子类型 ``` ### 6.2 应用场景 | 类型 | 方向 | 关键词 | 典型场景 | | --- | --- | --- | --- | | 协变 | 子→父 | 输出 | 方法返回值、数据提供者 | | 逆变 | 父→子 | 输入 | 方法参数、数据消费者 | --- ## 七、泛型的边界限制 ### 7.1 不能创建泛型数组 ```java List[] array = new List[10]; // ❌ 编译错误 ``` **原因:数组协变 + 泛型擦除 = 类型不安全** ```java // 假设允许创建泛型数组 List[] array = new List[10]; // 数组是协变的 Object[] objArray = array; // 可以放入List(运行时都是List) objArray[0] = new ArrayList(); // 取出时期望String,实际是Integer String s = array[0].get(0); // 💥 ClassCastException ``` **解决方案:** ```java // 方案1:使用集合(推荐) List> listOfLists = new ArrayList<>(); // 方案2:强制压制警告(不推荐) @SuppressWarnings("unchecked") List[] array = (List[]) new List[10]; ``` ### 7.2 不能实例化类型参数 ```java public class Box { private T value = new T(); // ❌ 编译错误 } ``` **原因:** 擦除后不知道T的具体类型,无法调用构造器 **解决方案:** ```java // 方案1:传入Class对象 public class Box { private T value; public Box(Class clazz) throws Exception { value = clazz.getDeclaredConstructor().newInstance(); } } // 方案2:使用工厂方法 public interface Factory { T create(); } public class Box { private T value; public Box(Factory factory) { value = factory.create(); } } ``` --- ## 八、高频面试题 ### Q1: 什么是泛型擦除?为什么要擦除? **答:** - **定义:** 编译后所有泛型信息被删除,字节码中只剩原始类型 - **原因:** 向后兼容Java 5之前的代码 - **影响:** 运行时无法获取泛型实际类型,不能创建泛型数组 ### Q2: `List` 和 `List` 运行时是同一个类型吗? **答:** 是的,都是`ArrayList.class`(擦除后) ### Q3: 为什么get()不需要强转? **答:** 编译器自动插入了`checkcast`指令(字节码层面的强转) ### Q4: 既然擦除了,为什么反射能获取泛型? **答:** `.class`文件中保留了Signature签名信息 - ✅ 可获取:成员变量、方法参数、方法返回值的泛型 - ❌ 无法获取:局部变量的泛型 ### Q5: 为什么泛型不支持基本类型? **答:** 擦除后变成`Object`,基本类型无法转为`Object`(需要装箱) ### Q6: `List` 为什么不能add? **答:** 编译器不知道具体是Integer还是Double,为防止类型污染,禁止写入 ### Q7: PECS原则怎么记? **答:** Producer Extends(生产者用extends),Consumer Super(消费者用super) ### Q8: 为什么不能创建泛型数组? **答:** 数组协变+泛型擦除会破坏类型安全 ### Q9: 什么是桥接方法? **答:** 编译器自动生成的方法,用于解决泛型擦除后重写方法参数类型不匹配的问题 ### Q10: 泛型在继承中的表现? **答:** `List` 不是 `List` 的子类(泛型不可变) --- ## 九、最佳实践 ### ✅ 推荐做法 ```java // 1. 优先使用具体类型 List list = new ArrayList<>(); // 2. 需要灵活性时用通配符 void print(List numbers) { } void add(List list) { } // 3. 工具类方法用泛型方法 public static T getFirst(List list) { } // 4. 避免原始类型 List list = new ArrayList<>(); // 而非 List list ``` ### ❌ 避免做法 ```java // 1. 不要使用原始类型 List list = new ArrayList(); // 失去泛型意义 // 2. 不要忽略编译警告 @SuppressWarnings("unchecked") // 除非真的知道自己在干嘛 // 3. 不要创建泛型数组 new List[10] // 非法 ``` --- ## 相关链接 - [[01-基本类型与包装类|01-基本类型与包装类]] - 为什么泛型不支持基本类型 - [[01-Class对象与反射机制|01-Class对象与反射机制]] - 反射如何突破泛型限制 - [[00-容器总览|集合框架]] - 泛型在集合中的应用 --- ## 记忆口诀 > 泛型擦除记心间,编译检查运行删 > 上界extends只能读,下界super才能写 > PECS原则要记牢,生产extends消费super > 桥接方法编译生,类型安全保无恙 > 数组泛型不能建,反射获取有限制 --- ⬅️ [[01-Class对象与反射机制|Class对象与反射机制]] 🏠 [[00-Java|00-Java]] ➡️ [[03-注解原理|注解原理]]