V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 12 章 / 共 20 章

泛型

类/方法泛型、通配符、PECS、类型擦除

为什么需要泛型

没有泛型时,集合存的都是 Object,存进去任何类型都行——但拿出来要强转,错了运行时才报 ClassCastException。泛型把类型作为参数声明出来,让编译器在写代码时就帮你查类型,避免强转、避免运行时崩溃。

  • 应用场景:所有集合(List<T>、Map<K,V>)、Optional<T>、CompletableFuture<T>、Comparator<T>
  • 自定义场景:通用容器(Pair<A,B>、Result<T>)、工具方法(找最大/最小/转换)、领域中的"持有任意类型"对象(Cache<K,V>、Repository<T>)
  • 核心好处:编译期类型安全 + 编写者免去强制转换
// WhyGenerics.java
import java.util.ArrayList;
import java.util.List;

public class WhyGenerics {
    public static void main(String[] args) {
        // 没泛型:拿出来要强转,出错运行时崩
        List rawList = new ArrayList();
        rawList.add("hello");
        rawList.add(42);                  // 编译器不挡
        String s = (String) rawList.get(1); // 运行时 ClassCastException

        // 有泛型:编译器拦截
        List<String> typed = new ArrayList<>();
        typed.add("hello");
        // typed.add(42);                 // 编译错误
        String s2 = typed.get(0);          // 不用强转
        System.out.println(s2);
    }
}

类泛型

类名后用 <T> 声明类型参数。常见场景:写一个"持有任意类型值"的容器。多个参数用逗号:<K, V>。

// Pair.java
public class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }

    public A first()  { return first; }
    public B second() { return second; }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }

    public static void main(String[] args) {
        Pair<String, Integer> p = new Pair<>("age", 30);
        System.out.println(p);                    // (age, 30)
        System.out.println(p.first().length());   // String 方法可用
        System.out.println(p.second() + 1);        // Integer 自动拆箱
    }
}

方法泛型

方法层面的泛型——和所属类的泛型独立。声明语法:返回类型前加 <T>。常见场景:通用工具方法,参数和返回都涉及同一类型。

// GenericMethod.java
import java.util.List;

public class GenericMethod {
    // <T> 是这个方法自己的类型参数
    static <T> T firstOrDefault(List<T> list, T fallback) {
        return list.isEmpty() ? fallback : list.get(0);
    }

    public static void main(String[] args) {
        List<String> names = List.of();
        List<Integer> nums  = List.of(1, 2, 3);

        System.out.println(firstOrDefault(names, "-"));    // -
        System.out.println(firstOrDefault(nums,  0));      // 1
    }
}

通配符 ? extends T 与 ? super T

List<Number> 不能接收 List<Integer> —— 这是 Java 泛型不变性的关键陷阱。要传"可变范围"的集合参数,用通配符:? extends T(上界,能读不能写)、? super T(下界,能写不能读)。

// Wildcards.java
import java.util.ArrayList;
import java.util.List;

public class Wildcards {
    // 只"读",能接受任何 Number 或其子类的 List
    static double sumAll(List<? extends Number> list) {
        double s = 0;
        for (Number n : list) s += n.doubleValue();
        return s;
    }

    // 只"写",能往里塞 Integer
    static void addInts(List<? super Integer> list, int n) {
        for (int i = 0; i < n; i++) list.add(i);
    }

    public static void main(String[] args) {
        List<Integer> ints = List.of(1, 2, 3);
        List<Double>  dbs  = List.of(1.5, 2.5);
        System.out.println(sumAll(ints));   // 6.0
        System.out.println(sumAll(dbs));    // 4.0

        List<Number> bucket = new ArrayList<>();
        addInts(bucket, 3);
        System.out.println(bucket);          // [0, 1, 2]
    }
}

PECS 原则

记住一句口诀:**Producer Extends, Consumer Super**。集合作为"数据源"(producer,你从中读)用 extends;作为"数据目标"(consumer,你往里写)用 super。标准库 Collections.copy(dst, src) 就是经典示例。

// Pecs.java
import java.util.ArrayList;
import java.util.List;

public class Pecs {
    // src 是 producer(被读)→ extends
    // dst 是 consumer(被写)→ super
    static <T> void copy(List<? super T> dst, List<? extends T> src) {
        for (T item : src) {
            dst.add(item);
        }
    }

    public static void main(String[] args) {
        List<Integer> src = List.of(1, 2, 3);
        List<Number> dst = new ArrayList<>();
        copy(dst, src);
        System.out.println(dst);  // [1, 2, 3]
    }
}

类型擦除与运行时影响

Java 泛型是"编译期类型检查 + 运行时擦除"——字节码里没有泛型信息,List<String> 和 List<Integer> 运行时是同一个 List 类。这导致一些限制:

  • 不能 new T()(不知道实际类型)
  • 不能创建泛型数组 new T[10]
  • 不能 obj instanceof List<String>(运行时拿不到 String 这层信息)
  • 类的 static 字段不能用类型参数 T
  • 反射拿到 List 的 Class 对象不带泛型;要拿带泛型的类型信息得用 Type / ParameterizedType
// Erasure.java
import java.util.ArrayList;
import java.util.List;

public class Erasure {
    public static void main(String[] args) {
        List<String> a = new ArrayList<>();
        List<Integer> b = new ArrayList<>();
        System.out.println(a.getClass() == b.getClass()); // true!同一个 ArrayList 类

        // if (a instanceof List<String>) {}  // 编译错误
        if (a instanceof List<?>) {            // 用 <?> 通配符即可
            System.out.println("is a list");
        }
    }
}

用 Class<T> 解决"拿不到运行时类型"

需要在运行时知道实际类型(比如做 JSON 反序列化、反射创建实例)时,惯用做法是让 caller 显式把 Class<T> 作为参数传进来。Jackson 的 readValue(json, User.class) 就是这个模式。

// ClassToken.java
public class ClassToken {
    static <T> T newInstance(Class<T> cls) throws Exception {
        return cls.getDeclaredConstructor().newInstance();
    }

    public static void main(String[] args) throws Exception {
        StringBuilder sb = newInstance(StringBuilder.class);
        sb.append("hello");
        System.out.println(sb);
    }
}

实战:通用 Result<T> 容器

应用场景:业务方法既可能成功(带数据)又可能失败(带错误码 / 消息),用 Result<T> 统一表达,比抛异常更友好(特别是 API 层)。

// Result.java
public class Result<T> {
    private final boolean ok;
    private final T data;
    private final String error;

    private Result(boolean ok, T data, String error) {
        this.ok = ok;
        this.data = data;
        this.error = error;
    }

    public static <T> Result<T> ok(T data)      { return new Result<>(true,  data, null); }
    public static <T> Result<T> fail(String msg) { return new Result<>(false, null, msg); }

    public boolean isOk()    { return ok; }
    public T       getData() { return data; }
    public String  getError(){ return error; }

    public static void main(String[] args) {
        Result<Integer> r1 = Result.ok(42);
        Result<Integer> r2 = Result.fail("divide by zero");

        for (Result<Integer> r : new Result[]{r1, r2}) {
            if (r.isOk()) System.out.println("data=" + r.getData());
            else          System.out.println("err=" + r.getError());
        }
    }
}