主题
界面
主题
集合的每一节都写了 List<String>、Map<String, Integer>——尖括号里的就是泛型。
本节回答三个问题:它解决了什么问题、怎么写、有哪些做不到的事(类型擦除带来的限制)。第三点尤其重要:它解释了为什么不能 new T[]、为什么 List<String> 与 List<Integer> 在运行时是同一个类。
完成本节后,你应该能够:
<T extends Number> 写带边界的泛型;? extends 与 ? super 的区别,并知道 PECS 原则;new T()、不能 new T[]、instanceof 不能用);List、Map 吗?(《List、Set、Map》)Object 是所有类的父类吗?(《继承与重写》)Java 5 之前没有泛型,集合只能存 Object:
List list = new ArrayList();
list.add("字符串");
list.add(42); // 编译通过!运行时会出问题
for (Object item : list) {
String text = (String) item; // 取出 42 时抛 ClassCastException
}问题在于:编译器明明能看出 42 不是字符串,但它不管——因为类型信息在放进集合时就被"擦掉"了。
泛型把这件事提前到编译期:
List<String> list = new ArrayList<>();
list.add("字符串");
list.add(42); // 编译错误:不兼容的类型核心价值:把运行时错误变成编译期错误。 编译期发现的问题改一行就好,运行时发现的问题要等上线、要排查、要回滚。
public class Box<T> { // T 是类型参数,使用时才确定
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> textBox = new Box<>("内容"); // T = String
Box<Integer> numberBox = new Box<>(42); // T = IntegerBox<T> 是"模板",Box<String> 才是"具体类型"。调用方指定类型,编译器帮你检查。
static <T> T first(List<T> list) { // 类型参数写在返回值之前
return list.get(0);
}
String value = first(List.of("a", "b")); // 编译器推断 T = String
int number = first(List.of(1, 2, 3)); // T = Integer泛型方法可以不依赖类:上面的 first 放在任何类里都能用,类型由调用处推断。
| 字母 | 含义 | 例子 |
|---|---|---|
T | Type(通用类型) | Box<T> |
E | Element(集合元素) | List<E> |
K / V | Key / Value | Map<K, V> |
R | Result(返回类型) | Function<T, R> |
? | 未知类型(通配符) | List<?> |
这些不是语法要求,而是约定——看到 E 就知道是"集合里的元素"。
static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T number : numbers) {
total += number.doubleValue(); // 因为知道 T 一定有 doubleValue()
}
return total;
}extends 在这里表示"上界"(可以是类或接口):T 必须是 Number 或其子类。
好处:在方法内部可以安全地调用 Number 的方法。没有边界时,T 只能当 Object 用。
注意:<T extends Number & Comparable<T>> 可以写多个上界,但类必须写在前面。
? extends 与 ? super // ? extends:只读(可以取出,不能放入)
static double sumAll(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) { // 取出时当成 Number
total += number.doubleValue();
}
return total;
}
// ? super:只写(可以放入,取出只能当 Object)
static void addIntegers(List<? super Integer> target) {
target.add(1); // 可以放入 Integer
target.add(2);
}PECS 原则(Producer Extends, Consumer Super):
| 场景 | 用法 | 记忆 |
|---|---|---|
| 你从集合读数据(生产者) | ? extends T | Producer Extends |
| 你往集合写数据(消费者) | ? super T | Consumer Super |
| 既读又写 | 用具体类型(不加通配符) |
sumAll(List<? extends Number>) 能接受 List<Integer>、List<Double>、List<Number>——这比写死 List<Number> 更灵活。
Box<String> textBox = new Box<>("内容");
Box<Integer> numberBox = new Box<>(42);
textBox.getClass() == numberBox.getClass(); // true!
textBox.getClass().getName(); // GenericsLesson$Box泛型只存在于编译期。编译后类型参数被"擦除"(erase)成它的上界(没有上界就是 Object),运行时的类里没有任何 String / Integer 的信息。
这解释了泛型的所有限制:
| 不能做的事 | 原因 |
|---|---|
new T() | 运行时不知道 T 是什么,无法调用构造方法 |
new T[10] | 同上 |
if (obj instanceof List<String>) | 运行时没有类型参数信息 |
static T field; | 静态成员属于类,而类型参数属于实例 |
重载 foo(List<String>) 与 foo(List<Integer>) | 擦除后签名相同,冲突 |
为什么 Java 要这么做:为了向后兼容——Java 5 之前的代码用的是 List(原生类型),如果泛型保留到运行时,老代码就全崩了。这是一个工程折中。
// 需要运行时类型信息时:传 Class 对象
static <T> T create(Class<T> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
// 需要泛型数组时:用 List<T> 替代,或传 Class<T> 辅助创建这是 《反射基础》 反射 会展开的内容。
Box<String> box = new Box<>("内容");
System.out.println(box.get().length()); // 取出即可用,无需强转public class Result<T> {
private final boolean success;
private final T data;
private final String error;
public T getData() { return data; }
}
Result<List<Order>> result = service.query(); // 类型信息一路传递
List<Order> orders = result.getData(); // 不需要强转这是真实项目里最常见的泛型用法:让类型信息沿着调用链传下去。
// 错误一:泛型数组
T[] array = new T[10]; // 编译错误
// 错误二:instanceof 带类型参数
if (obj instanceof List<String>) { } // 编译错误
// 错误三:静态字段用类型参数
class Box<T> {
static T defaultValue; // 编译错误
}文件:labs/collections/src/main/java/cn/kb/learn/collection/GenericsLesson.java
package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.List;
public class GenericsLesson {
/** 泛型类:类型参数由使用者指定 */
static class Box<T> {
private T value;
Box(T value) {
this.value = value;
}
T get() {
return value;
}
void set(T value) {
this.value = value;
}
@Override
public String toString() {
return "Box[" + value + "]";
}
}
/** 泛型方法:类型参数写在返回值之前 */
static <T> T first(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("列表不能为空");
}
return list.get(0);
}
/** 泛型边界:只接受 Number 及其子类 */
static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T number : numbers) {
total += number.doubleValue();
}
return total;
}
/** 通配符上界:适合"只能读"的场景 */
static double sumAll(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}
/** 通配符下界:适合"只能写"的场景 */
static void addIntegers(List<? super Integer> target) {
target.add(1);
target.add(2);
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
// 1. 不用泛型:把类型检查推到了运行时
List rawList = new ArrayList();
rawList.add("字符串");
rawList.add(42);
try {
for (Object item : rawList) {
String text = (String) item;
System.out.println("取出并转为字符串: " + text);
}
} catch (ClassCastException exception) {
System.out.println("原生类型导致运行时错误: ClassCastException(取出 42 时)");
}
// 2. 泛型类:编译期就知道类型
Box<String> textBox = new Box<>("内容");
Box<Integer> numberBox = new Box<>(42);
System.out.println(textBox + " / " + numberBox);
System.out.println("取出即用,无需强转,长度: " + textBox.get().length());
// 3. 泛型方法:调用时自动推断
System.out.println("第一个元素: " + first(List.of("a", "b")));
System.out.println("第一个数字: " + first(List.of(1, 2, 3)));
// 4. 泛型边界
System.out.println("整数求和: " + sum(List.of(1, 2, 3)));
System.out.println("小数求和: " + sum(List.of(1.5, 2.5)));
// 5. 通配符
System.out.println("通配符读取: " + sumAll(List.of(1, 2.5, 3L)));
List<Number> numbers = new ArrayList<>();
addIntegers(numbers);
System.out.println("通配符写入: " + numbers);
// 6. 类型擦除:运行时不保留类型参数
System.out.println("运行时类型: " + textBox.getClass().getName());
System.out.println("Box<String> 与 Box<Integer> 是同一个类: " + (textBox.getClass() == numberBox.getClass()));
}
}运行命令:
cd labs
./mvnw -pl collections compile
java -cp collections/target/classes cn.kb.learn.collection.GenericsLesson预期输出:
取出并转为字符串: 字符串
原生类型导致运行时错误: ClassCastException(取出 42 时)
Box[内容] / Box[42]
取出即用,无需强转,长度: 2
第一个元素: a
第一个数字: 1
整数求和: 6.0
小数求和: 4.0
通配符读取: 6.5
通配符写入: [1, 2]
运行时类型: cn.kb.learn.collection.GenericsLesson$Box
Box<String> 与 Box<Integer> 是同一个类: true前两行与最后一行的对比是本节的核心:
Box<String> 与 Box<Integer> 是同一个类)。泛型的价值不在运行时,而在编译期。
textBox.getClass() == numberBox.getClass() 处断点,观察两个对象的类完全相同——擦除的直接证据;first(List.of("a", "b")) 处 F7 进入 first,观察参数 list 在调试器里的类型是 List(没有 <String>)——调试器也不保留泛型信息;sum 传入 List.of("a"),观察编译错误信息("类型参数 String 不在类型变量 T 的范围内");new T[10](在泛型类内部),观察编译错误,理解擦除带来的限制。List list = new ArrayList(); // 原生类型:丢掉所有类型检查
list.add("a");
list.add(42); // 编译通过,运行时可能出错修复:始终写明类型参数。IDE 会给出 "Raw use of parameterized class" 警告。
List<Object> 能接收 List<String> List<Object> objects = new ArrayList<String>(); // 编译错误原因:泛型不支持协变。String 是 Object 的子类,但 List<String> 不是 List<Object> 的子类——否则就能往里放 Integer,破坏类型安全。
修复:用通配符 List<? extends Object>(等价于 List<?>)。
if (list instanceof List<String>) { } // 编译错误
if (list instanceof List<?>) { } // 正确:只能判断是不是 Liststatic void addAll(List<? extends Number> target, Number value) {
target.add(value); // 编译错误
}修复:需要写入时用 ? super T(PECS 原则)。
目标:写一个泛型工具类,并体会擦除的限制。
步骤:
<T> List<T> filter(List<T> source, Predicate<T> predicate):返回满足条件的元素(不修改入参);Pair<K, V>:持有两个值,提供 key() 与 value() 方法,并支持"交换"(返回新的 Pair<V, K>);<T extends Comparable<T>> T max(List<T> values):返回最大值;T[] array = new T[10],记录编译错误信息,并写出两种替代方案。验收方式:能解释第 5 步的报错原因与替代方案。
扩展任务:对比 List<? extends Number> 与 List<Number> 作为参数时的差别——分别用 List<Integer>、List<Double>、List<Number> 调用,看哪些能通过编译。这能帮你彻底理解"通配符放宽了什么"。
基础题
List<String> 与 List<Integer> 在运行时是不同的类。(对 / 错)代码阅读题
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
List<Number> numbers = new ArrayList<>();
copy(numbers, List.of(1, 2.5));
System.out.println(numbers);copy 的参数为什么这样设计(提示:这是 Collections.copy 的真实签名)?编程题
Cache<K, V>: Map<K, V> 存储;V get(K key, Function<K, V> loader):缓存命中直接返回,未命中则调用 loader 计算并缓存;void put(K key, V value)、V getIfPresent(K key)、int size();综合任务
PageResult<T>:包含 List<T> records、long total、int page、int size;int totalPages()、boolean hasNext() 两个方法;<T> PageResult<T> of(List<T> records, long total, int page, int size);PageResult<String> mapToString() 或类似的转换方法,说明为什么转换后类型参数会变;PageResult<Order> 与 PageResult<String> 各创建一个实例验证。提示 1(方向):第 4 题的关键是 PECS——src 是生产者(读),dest 是消费者(写)。
提示 2(关键点):第 5 题的 get 方法用 computeIfAbsent 实现只需一行:return map.computeIfAbsent(key, loader)。
提示 3(接近答案):第 6 题的 mapToString 可以写成实例方法 PageResult<R> map(Function<T, R> mapper),用 records.stream().map(mapper).toList() 生成新结果。
List<String> 与 List<Integer> 在运行时都是 List(同一个类)。泛型只存在于编译期。extends、? extends T(PECS:Producer Extends)、? super T(Consumer Super)。输出 [1, 2.5]。
参数设计体现了 PECS:
src 是生产者(从它读取元素)→ 用 ? extends T,这样 List<Integer> 也能传进来;dest 是消费者(向它写入元素)→ 用 ? super T,这样 List<Object>、List<Number> 都能作为目标。如果写成 copy(List<T> dest, List<T> src),那么 copy(numbers, List.of(1, 2.5)) 就无法编译——因为 List.of(1, 2.5) 的类型是 List<Number>,而 numbers 也是 List<Number>,看起来能行;但换成 copy(numbers, List.of(1, 2))(List<Integer>)就不行了。
这就是通配符的价值:让方法接受更宽泛的实参类型,同时保持类型安全。
package cn.kb.learn.collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class Cache<K, V> {
private final Map<K, V> store = new HashMap<>();
/** 命中直接返回,未命中用 loader 计算并缓存 */
public V get(K key, Function<K, V> loader) {
return store.computeIfAbsent(key, loader);
}
public void put(K key, V value) {
store.put(key, value);
}
public Optional<V> getIfPresent(K key) {
return Optional.ofNullable(store.get(key));
}
public int size() {
return store.size();
}
public static void main(String[] args) {
Cache<String, Integer> wordLength = new Cache<>();
System.out.println("第一次(计算): " + wordLength.get("java", String::length));
System.out.println("第二次(命中缓存): " + wordLength.get("java", key -> {
System.out.println(" 不会执行到这里");
return -1;
}));
System.out.println("缓存大小: " + wordLength.size());
System.out.println("不存在的 key: " + wordLength.getIfPresent("missing"));
}
}泛型带来的好处:Cache<String, Integer> 在编译期就保证"key 是字符串、value 是整数",取出时不用强转;同时这个类可以复用在任何 key/value 组合上(Cache<Long, Order>、Cache<String, List<User>>),代码只写一次。
预期输出:
第一次(计算): 4
第二次(命中缓存): 4
缓存大小: 1
不存在的 key: Optional.emptypackage cn.kb.learn.collection;
import java.util.List;
import java.util.function.Function;
public class PageResult<T> {
private final List<T> records;
private final long total;
private final int page;
private final int size;
private PageResult(List<T> records, long total, int page, int size) {
this.records = List.copyOf(records);
this.total = total;
this.page = page;
this.size = size;
}
public static <T> PageResult<T> of(List<T> records, long total, int page, int size) {
return new PageResult<>(records, total, page, size);
}
/** 把每条记录转换成另一种类型,返回新的分页结果 */
public <R> PageResult<R> map(Function<T, R> mapper) {
return PageResult.of(records.stream().map(mapper).toList(), total, page, size);
}
public int totalPages() {
return size <= 0 ? 0 : (int) Math.ceil((double) total / size);
}
public boolean hasNext() {
return (long) page * size < total;
}
public List<T> records() {
return records;
}
@Override
public String toString() {
return "PageResult{records=" + records + ", total=" + total
+ ", page=" + page + ", size=" + size + "}";
}
public static void main(String[] args) {
PageResult<String> names = PageResult.of(List.of("张三", "李四"), 5, 1, 2);
System.out.println(names);
System.out.println("总页数: " + names.totalPages() + ",还有下一页: " + names.hasNext());
PageResult<Integer> lengths = names.map(String::length);
System.out.println("转换后类型参数变成 Integer: " + lengths);
}
}为什么转换后类型参数会变:map 方法的签名是 <R> PageResult<R> map(Function<T, R> mapper)——它把 T 转成了 R,所以返回的是 PageResult<R>。类型参数沿着转换链传播,编译器全程跟踪,这就是泛型在 API 设计里的核心作用。
预期输出:
PageResult{records=[张三, 李四], total=5, page=1, size=2}
总页数: 3,还有下一页: true
转换后类型参数变成 Integer: PageResult{records=[2, 2], total=5, page=1, size=2}extends 写带边界的泛型;? extends 与 ? super 的区别;《异常体系与处理》:把 《读懂 Java 错误信息》 的"读异常"升级为"设计异常"。
暂无