跳到内容

12.1 (上):类型参数与泛型方法

学过类、接口、集合与异常后,就可以用约 35 分钟跑完这篇 Java 17 示例。读完应能声明泛型类、record、接口和方法,并用上界准确表达“这个类型必须具备什么能力”。

老陈师傅让你为工具箱写“取出物品”的方法。木锤箱返回 String,零件箱返回 Part,账本箱又返回 Ledger。三个箱子的结构完全相同,只有物品类型不同。

“复制箱子不难,”他敲了敲箱盖,“难的是以后每改一把锁,都得记得改三次。”

把所有物品都声明成 Object 也不理想:调用者每次取出都要强制转换,放错物品要到运行时才暴露。泛型选择另一条路——让类型成为声明的一部分,由调用者在使用时提供。

1. raw type 把错误推迟到运行时

阿花把一张数字库存单塞进“工具名”抽屉,抽屉没有阻止她。直到你按字符串取出第二项,错误才爆出来。raw type 就像没写用途的旧木箱:什么都装得下,也把诊断拖到了最晚的时候。

旧式 raw List 不记录元素类型:

java
List raw = new ArrayList();
raw.add("Hammer");
raw.add(42);

String tool = (String) raw.get(1); // 运行时 ClassCastException

错误并不在最后一行才发生。真正的污染点是整数被写进本应只保存工具名的集合。参数化类型把诊断推回写入处:

java
List<String> tools = new ArrayList<>();
tools.add("Hammer");
// tools.add(42); // 编译失败

迁移旧代码时不要用 @SuppressWarnings("unchecked") 盖住整个类。先定位无法证明安全的转换,把检查和抑制缩到最小边界。

2. Box<T> 只声明一次

老陈把三个箱子的重复木板拆下来,只留下一个结构,再把“里面装什么”写成参数 T。箱子还是同一套设计,调用者却必须在领用时把物品类型说清楚。

java
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

java
Box<String> toolBox = new Box<>("Hammer");
String tool = toolBox.value();

右侧 <> 是 diamond。编译器从赋值目标和构造参数推断类型,但推断不等于“运行时自动识别一切”。若上下文不足或重载相互竞争,仍需显式写出类型实参。

3. record 与接口也能参数化

二元结果可以保留两个独立类型:

java
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() 返回 Stringvalue() 返回 Integer。record 只是替你生成值类型样板,并不会改变泛型规则。

接口可以把类型选择留给实现类:

java
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 方法声明中:

java
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. 泛型方法在返回类型前声明参数

java
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")) 时,编译器推断 TString。类型推断依据声明与调用上下文,不会检查方法体以猜测一个随意的返回类型。

6. 上界说明“这个类型必须会什么”

工具箱能装任意 T,可仓库里的“选出最大件”还需要一项能力:两件物品必须能够比较。老陈没有列出允许的每一种材料,而是把这项能力写进类型上界。

取最大值需要比较能力:

java
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. 在工作台上跑通基础契约

木箱、二元领料记录和“取第一件”方法已经分别讲过,现在把它们放到同一个工作台。编译器负责拦住类型不合的物品,断言负责检查运行结果;两道检查解决的问题不同。

先准备临时目录:

bash
chapter12_basics_root=$(mktemp -d)
readonly chapter12_basics_root
mkdir -p "$chapter12_basics_root/out"
cd "$chapter12_basics_root"

保存为 GenericsBasicsDemo.java

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) { }
}
bash
javac --release 17 -Xlint:all -d out GenericsBasicsDemo.java
java -cp out GenericsBasicsDemo

预期输出 泛型基础检查通过。保存记录后清理:

bash
cd
ls -ld -- "$chapter12_basics_root"
rm -r -- "$chapter12_basics_root"

离开工具箱前

区分类型参数与类型实参,说明 diamond 的推断来源,并解释 static 方法为什么必须声明自己的类型参数。最后拿一个父类实现 Comparable<Parent>、子类继承它的例子,验证 Comparable<? super T> 的意义。

下一页处理最容易混淆的部分:为什么 List<Integer> 不能直接交给 List<Number>,以及 extendssuper 各允许什么。

下一步:不变性、通配符与 PECS

规范入口:JLS §4.5:参数化类型JLS §8.4.4:泛型方法

Built with VitePress | Software Systems Atlas