12.1 (上):类型参数与泛型方法
学过类、接口、集合与异常后,就可以用约 35 分钟跑完这篇 Java 17 示例。读完应能声明泛型类、record、接口和方法,并用上界准确表达“这个类型必须具备什么能力”。
老陈师傅让你为工具箱写“取出物品”的方法。木锤箱返回 String,零件箱返回 Part,账本箱又返回 Ledger。三个箱子的结构完全相同,只有物品类型不同。
“复制箱子不难,”他敲了敲箱盖,“难的是以后每改一把锁,都得记得改三次。”
把所有物品都声明成 Object 也不理想:调用者每次取出都要强制转换,放错物品要到运行时才暴露。泛型选择另一条路——让类型成为声明的一部分,由调用者在使用时提供。
1. raw type 把错误推迟到运行时
阿花把一张数字库存单塞进“工具名”抽屉,抽屉没有阻止她。直到你按字符串取出第二项,错误才爆出来。raw type 就像没写用途的旧木箱:什么都装得下,也把诊断拖到了最晚的时候。
旧式 raw List 不记录元素类型:
List raw = new ArrayList();
raw.add("Hammer");
raw.add(42);
String tool = (String) raw.get(1); // 运行时 ClassCastException错误并不在最后一行才发生。真正的污染点是整数被写进本应只保存工具名的集合。参数化类型把诊断推回写入处:
List<String> tools = new ArrayList<>();
tools.add("Hammer");
// tools.add(42); // 编译失败迁移旧代码时不要用 @SuppressWarnings("unchecked") 盖住整个类。先定位无法证明安全的转换,把检查和抑制缩到最小边界。
2. Box<T> 只声明一次
老陈把三个箱子的重复木板拆下来,只留下一个结构,再把“里面装什么”写成参数 T。箱子还是同一套设计,调用者却必须在领用时把物品类型说清楚。
final class Box<T> {
private final T value;
Box(T value) {
this.value = java.util.Objects.requireNonNull(value, "value");
}
T value() {
return value;
}
}T 是类型参数;Box<String> 中的 String 是类型实参。编译器据此把 value() 的返回类型视为 String:
Box<String> toolBox = new Box<>("Hammer");
String tool = toolBox.value();右侧 <> 是 diamond。编译器从赋值目标和构造参数推断类型,但推断不等于“运行时自动识别一切”。若上下文不足或重载相互竞争,仍需显式写出类型实参。
3. record 与接口也能参数化
二元结果可以保留两个独立类型:
record Pair<K, V>(K key, V value) {
Pair {
java.util.Objects.requireNonNull(key, "key");
java.util.Objects.requireNonNull(value, "value");
}
}Pair<String, Integer> 的 key() 返回 String,value() 返回 Integer。record 只是替你生成值类型样板,并不会改变泛型规则。
接口可以把类型选择留给实现类:
interface Repository<ID, E> {
java.util.Optional<E> find(ID id);
void save(ID id, E entity);
}
final class ToolRepository implements Repository<String, String> {
private final java.util.Map<String, String> values = new java.util.HashMap<>();
public java.util.Optional<String> find(String id) {
return java.util.Optional.ofNullable(values.get(id));
}
public void save(String id, String tool) {
values.put(id, tool);
}
}4. static 成员没有类的 T
Box<String> 与 Box<Integer> 共享同一份 static 状态。因此类的类型参数不能直接出现在 static 字段或 static 方法声明中:
final class Box<T> {
// static T lastValue; // 编译失败
static <E> Box<E> of(E value) {
return new Box<>(value);
}
private final T value;
private Box(T value) { this.value = value; }
T value() { return value; }
}of 声明了自己的方法类型参数 E。它和类级 T 没有继承关系,只是名字可以碰巧相同;实际代码用不同字母能减少误读。
5. 泛型方法在返回类型前声明参数
static <T> T first(java.util.List<T> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("empty values");
}
return values.get(0);
}调用 first(List.of("iron", "wood")) 时,编译器推断 T 为 String。类型推断依据声明与调用上下文,不会检查方法体以猜测一个随意的返回类型。
6. 上界说明“这个类型必须会什么”
工具箱能装任意 T,可仓库里的“选出最大件”还需要一项能力:两件物品必须能够比较。老陈没有列出允许的每一种材料,而是把这项能力写进类型上界。
取最大值需要比较能力:
static <T extends Comparable<? super T>> T max(java.util.List<T> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("empty values");
}
T result = values.get(0);
for (int index = 1; index < values.size(); index++) {
if (values.get(index).compareTo(result) > 0) {
result = values.get(index);
}
}
return result;
}Comparable<? super T> 比 Comparable<T> 更宽:如果父类已经定义了与父类型比较的规则,子类列表也能使用。多个上界写成 <T extends Base & Audited & Comparable<T>>,类上界若存在必须排在最左侧。
7. 在工作台上跑通基础契约
木箱、二元领料记录和“取第一件”方法已经分别讲过,现在把它们放到同一个工作台。编译器负责拦住类型不合的物品,断言负责检查运行结果;两道检查解决的问题不同。
先准备临时目录:
chapter12_basics_root=$(mktemp -d)
readonly chapter12_basics_root
mkdir -p "$chapter12_basics_root/out"
cd "$chapter12_basics_root"保存为 GenericsBasicsDemo.java:
import java.util.List;
import java.util.Objects;
public class GenericsBasicsDemo {
public static void main(String[] args) {
Box<String> box = Box.of("Hammer");
checkEquals("Hammer", box.value());
Pair<String, Integer> stock = new Pair<>("iron", 12);
checkEquals("iron", stock.key());
checkEquals(12, stock.value());
checkEquals("iron", first(List.of("iron", "wood")));
checkEquals(9, max(List.of(3, 9, 5)));
expectIllegal(() -> first(List.of()));
System.out.println("泛型基础检查通过");
}
static <T> T first(List<T> values) {
if (values.isEmpty()) throw new IllegalArgumentException("empty values");
return values.get(0);
}
static <T extends Comparable<? super T>> T max(List<T> values) {
if (values.isEmpty()) throw new IllegalArgumentException("empty values");
T result = values.get(0);
for (int index = 1; index < values.size(); index++) {
if (values.get(index).compareTo(result) > 0) result = values.get(index);
}
return result;
}
static void expectIllegal(Runnable action) {
try {
action.run();
throw new AssertionError("expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// 空输入契约按预期失败。
}
}
static void checkEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + ", actual=" + actual);
}
}
static final class Box<T> {
private final T value;
private Box(T value) { this.value = Objects.requireNonNull(value); }
static <E> Box<E> of(E value) { return new Box<>(value); }
T value() { return value; }
}
record Pair<K, V>(K key, V value) { }
}javac --release 17 -Xlint:all -d out GenericsBasicsDemo.java
java -cp out GenericsBasicsDemo预期输出 泛型基础检查通过。保存记录后清理:
cd
ls -ld -- "$chapter12_basics_root"
rm -r -- "$chapter12_basics_root"离开工具箱前
区分类型参数与类型实参,说明 diamond 的推断来源,并解释 static 方法为什么必须声明自己的类型参数。最后拿一个父类实现 Comparable<Parent>、子类继承它的例子,验证 Comparable<? super T> 的意义。
下一页处理最容易混淆的部分:为什么 List<Integer> 不能直接交给 List<Number>,以及 extends、super 各允许什么。