12.1 类型参数与泛型方法
学过类、接口、集合与异常后,就可以用约 35 分钟跑完这篇 Java 17 示例。读完应能声明泛型类、record、接口和方法,并用上界准确表达“这个类型必须具备什么能力”。
三个容器分别返回 String、Part 和 Ledger,结构完全相同,只有元素类型不同。复制三份实现不难,难的是以后每次修改都要保持三份一致。
把所有物品都声明成 Object 也不理想:调用者每次取出都要强制转换,放错物品要到运行时才暴露。泛型选择另一条路,让类型成为声明的一部分,由调用者在使用时提供。
1. raw type 把错误推迟到运行时
旧式 raw List 不约束元素类型。以下片段需导入 java.util 中的相关类型;raw 示例单独编译,预期有警告并在运行时失败。不同版本的 Box 也要分开放:
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<>();
@Override
public java.util.Optional<String> find(String id) {
java.util.Objects.requireNonNull(id, "id");
return java.util.Optional.ofNullable(values.get(id));
}
@Override
public void save(String id, String tool) {
values.put(java.util.Objects.requireNonNull(id, "id"),
java.util.Objects.requireNonNull(tool, "tool"));
}
}这里拒绝 null ID 和 null 值,让 Optional.empty 只表示找不到;重复 ID 会替换旧值。这个内存仓库没有持久化和并发写入保证。
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 = java.util.Objects.requireNonNull(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。类型推断依据声明与调用上下文,不会检查方法体以猜测一个随意的返回类型。first 要求列表非 null 且非空,返回元素本身,不额外拒绝首元素为 null。
6. 上界说明“这个类型必须会什么”
取最大值需要比较能力:
static <T extends Comparable<? super T>> T max(java.util.List<T> values) {
java.util.Iterator<T> iterator = values.iterator();
if (!iterator.hasNext()) {
throw new IllegalArgumentException("empty values");
}
T result = java.util.Objects.requireNonNull(iterator.next(), "element");
while (iterator.hasNext()) {
T candidate = java.util.Objects.requireNonNull(iterator.next(), "element");
if (candidate.compareTo(result) > 0) result = candidate;
}
return result;
}Comparable<? super T> 比 Comparable<T> 更宽:如果父类已经定义了与父类型比较的规则,子类列表也能使用。max 则要求每个元素非 null;使用 Iterator 保持单次遍历,避免 LinkedList 上反复 get(index) 导致二次方访问成本。下面的子类继承 Comparable<Ranked>,仍能作为 max 的 T:
class Ranked implements Comparable<Ranked> {
final int rank;
Ranked(int rank) { this.rank = rank; }
@Override public int compareTo(Ranked other) {
return Integer.compare(rank, other.rank);
}
}
final class SpecialRanked extends Ranked {
SpecialRanked(int rank) { super(rank); }
}
// 以下调用放在与 max 同一类的方法中:
SpecialRanked best = max(java.util.List.of(
new SpecialRanked(2), new SpecialRanked(5)));
System.out.println(best.rank); // 5多个上界写成 <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(java.util.List<T> values) {
java.util.Iterator<T> iterator = values.iterator();
if (!iterator.hasNext()) {
throw new IllegalArgumentException("empty values");
}
T result = java.util.Objects.requireNonNull(iterator.next(), "element");
while (iterator.hasNext()) {
T candidate = java.util.Objects.requireNonNull(iterator.next(), "element");
if (candidate.compareTo(result) > 0) result = candidate;
}
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) {
Pair {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(value, "value");
}
}
}javac --release 17 -encoding UTF-8 -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 各允许什么。