跳到内容

第 1 卷 / 第 12 章 / 第 1 课

12.1 类型参数与泛型方法 ​

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

三个容器分别返回 String、Part 和 Ledger,结构完全相同,只有元素类型不同。复制三份实现不难,难的是以后每次修改都要保持三份一致。

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

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

旧式 raw List 不约束元素类型。以下片段需导入 java.util 中的相关类型;raw 示例单独编译,预期有警告并在运行时失败。不同版本的 Box 也要分开放:

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() 返回 String,value() 返回 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<>();

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

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 = java.util.Objects.requireNonNull(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")) 时,编译器推断 T 为 String。类型推断依据声明与调用上下文,不会检查方法体以猜测一个随意的返回类型。first 要求列表非 null 且非空,返回元素本身,不额外拒绝首元素为 null。

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

取最大值需要比较能力:

java
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:

java
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. 跑通基础契约 ​

把泛型容器、二元记录和“取第一项”方法放进同一个可运行示例。编译器负责拦住类型不合的值,断言负责检查运行结果;两道检查解决的问题不同。

先准备临时目录:

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(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");
        }
    }
}
bash
javac --release 17 -encoding UTF-8 -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>,以及 extends、super 各允许什么。

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

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

Built with VitePress | Software Systems Atlas