ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
下面用“从入门到进阶”的顺序,把 **Java 泛型容器(Generic Collections)** 彻底拆开讲一遍。读完你能回答以下问题: - 为什么需要泛型? - 背后擦除机制到底做了什么? - 怎样写自己的泛型容器? - 常见通配符 `? extends` / `? super` 到底怎么用? - 性能、反射、序列化等实战陷阱有哪些? --- ### 1. 没有泛型之前(Java 1.4 及以前) ```java List list = new ArrayList(); // 只能存 Object list.add("hello"); String s = (String) list.get(0); // 强制转型 + 运行时才能发现错误 ``` 痛点 - 代码里到处是强制转型。 - 运行时才抛 `ClassCastException`。 --- ### 2. 泛型登场(Java 5) ```java List<String> list = new ArrayList<>(); // 类型参数 <String> list.add("hello"); String s = list.get(0); // 编译器已保证安全,无需强转 ``` 编译器在 **编译期** 帮你完成类型检查;运行时 **类型擦除**(type erasure)将 `List<String>` 还原成裸 `List`,里面仍然是 `Object`。 --- ### 3. 类型擦除内幕(面试高频) - 擦除到“上界” - `List<T>` → `List<Object>` - `List<T extends Number>` → `List<Number>` - 插入桥方法(bridge method)保持多态 - 编译器会生成 `public Object get(int)` 桥接 `public T get(int)` - 不能出现基本类型作为参数 - `List<int>` ❌ → `List<Integer>` ✅ - 不能通过反射拿到泛型实参(运行时没有),但 `Field.getGenericType()` 可以拿到 **声明时** 的信息。 --- ### 4. 自己写一个泛型容器(最小可用版) ```java public class MyStack<E> { // E = Element private Object[] elements; private int top = 0; public MyStack(int capacity) { elements = new Object[capacity]; // 不能直接 new E[] } public void push(E e) { elements[top++] = e; } @SuppressWarnings("unchecked") public E pop() { return (E) elements[--top]; } } ``` 要点 - 不能 `new E[]`,只能用 `Object[]` 再强转。 - `@SuppressWarnings("unchecked")` 告诉编译器“我保证安全”。 --- ### 5. 泛型方法 & 静态工具 ```java public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } ``` 调用 ```java Integer m = Util.max(1, 3); // 自动推断 T=Integer ``` --- ### 6. 通配符:PECS 原则(Producer-Extends, Consumer-Super) | 通配符 | 读 | 写 | 场景 | |--------|----|----|------| | `List<? extends Fruit>` | 能读,返回 `Fruit` 或其子类 | 不能写(除了 `null`) | **生产者** | | `List<? super Apple>` | 能读,返回 `Object` | 能写 `Apple` 及其子类 | **消费者** | | `List<?>` 无界 | 只能读 `Object` | 不能写(除了 `null`) | 只读 | 示例 ```java // 拷贝 src → dest,dest 消费数据 → 用 super public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T t : src) dest.add(t); } ``` --- ### 7. 常见实现 & 线程安全版本 | 接口 | 主要实现类 | 线程安全版 | 备注 | |------|------------|------------|------| | `List<E>` | `ArrayList`, `LinkedList` | `CopyOnWriteArrayList`, `Collections.synchronizedList` | 随机访问 vs 插入删除 | | `Set<E>` | `HashSet`, `LinkedHashSet`, `TreeSet` | `CopyOnWriteArraySet`, `ConcurrentSkipListSet` | 去重、排序 | | `Map<K,V>` | `HashMap`, `LinkedHashMap`, `TreeMap` | `ConcurrentHashMap`, `ConcurrentSkipListMap` | key 唯一、排序 | --- ### 8. 高级话题 #### 8.1 类型令牌绕过擦除(TypeToken) ```java Type listOfString = new TypeToken<List<String>>(){}.getType(); List<String> list = new Gson().fromJson(json, listOfString); ``` 利用匿名子类在编译期把 `List<String>` 存进 `.class` 常量池,运行时再读出来。 #### 8.2 可变参数 + 泛型警告 ```java public static <T> void addAll(Collection<T> c, T... elements) { Collections.addAll(c, elements); } ``` 编译器会提示 “Possible heap pollution”——本质上是 `T[]` 与 `Object[]` 协变带来的隐患。 #### 8.3 序列化陷阱 - 擦除后只有 `List<Object>`,序列化框架(如 Jackson)需借助 `TypeReference` 或 `TypeToken` 才能正确反序列化。 --- ### 9. 速查口诀 1. **声明**:类/接口后加 `<T>`,方法返回值前加 `<T>`。 2. **擦除**:运行时裸 raw 类型,无实参信息。 3. **通配符**:往里写用 `super`,往外读用 `extends`。 4. **不能**:实例化 `new T()`、`new T[]`、`T.class`。 5. **可变参数**:`T...` 会警告,谨慎使用。 --- ### 10. 小结 - **泛型容器 = 容器 + 编译期类型检查 + 运行时擦除**。 - 90% 的场景只需要 `<E>`、`<K,V>` 以及 `<? extends / super>` 三种语法糖;剩下 10% 的坑来自反射、序列化、数组协变等深层机制。 - 面试常问:擦除原理、PECS、为什么不能 `new T[]`,如何绕过。 把上面的代码片段复制到 IDE 里跑一遍,基本就能把泛型容器吃透了。