泛型
类/方法泛型、通配符、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());
}
}
}