跳到正文
Elaine Blog
返回

泛型、边界与类型擦除

Java 基础

**泛型(Generics)**让类和方法把“操作哪种类型”作为参数。它的主要价值是把错误提前到编译期,并减少不安全的强制 类型转换。

Table of contents

Open Table of contents

从类型安全仓库开始

import java.util.*;

interface 有标识<ID> { ID id(); }

final class 内存仓库<ID, T extends 有标识<ID>> {
    private final Map<ID, T> data = new HashMap<>();
    void 保存(T entity) { data.put(entity.id(), entity); }
    Optional<T> 查找(ID id) { return Optional.ofNullable(data.get(id)); }
}

record 用户(Long id, String name) implements 有标识<Long> {}

T extends 有标识<ID>上界(Upper Bound)T 可以是该接口的任意实现,但必须能提供 ID。因此仓库无需 反射也能取得实体标识。

理解不变性

即使 IntegerNumber 的子类,List<Integer> 也不是 List<Number> 的子类。否则调用方可以向“数字列表”加入 Double,破坏原列表只含整数的承诺。这叫泛型的不变性(Invariance)

通配符用于表达只读或只写方向:

static double 求和(List<? extends Number> source) {
    return source.stream().mapToDouble(Number::doubleValue).sum();
}

static void 加入默认值(List<? super Integer> target) {
    target.add(0);
}

记忆 PECS:Producer Extends,Consumer Super。作为数据生产者的参数用 ? extends T;作为消费者用 ? super T。若既读又写,通常直接使用 List<T>

类型擦除带来的限制

Java 泛型主要在编译期工作,运行时多数类型参数会被擦除(Type Erasure)。因此不能写:

// new T()
// T.class
// new List<String>[10]
// if (value instanceof List<String>)

需要在运行时保留类型时,显式传入 Class<T>、工厂 Supplier<T>,或框架提供的类型令牌。例如:

static <T> T 创建(java.util.function.Supplier<T> factory) {
    return factory.get();
}
var names = 创建(java.util.ArrayList<String>::new);

不要用原始类型 List 绕过编译器。它会把本应在写代码时发现的问题推迟成生产环境的 ClassCastException

API 设计检查

下一步

阅读Lambda、Stream 与副作用边界,把集合处理写成可读的数据管道。


分享这篇文章:

上一篇
集合框架的选择与复杂度
下一篇
Lambda、Stream 与副作用边界