Java 基础体系 · 第 6/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java 泛型完整基础:类型擦除、通配符、PECS、边界和反射
1. 泛型解决的是什么问题
泛型(Generics)允许类、接口和方法把“某个类型”作为参数传入,从而在编译期建立更精确的类型约束。
没有泛型时,集合通常只能以 Object 接收元素:
import java.util.ArrayList;
import java.util.List;
List names = new ArrayList();
names.add("Ada");
names.add(42);
String name = (String) names.get(0);
这里有两个问题:
List可以混入任何引用类型;- 取值时必须手动强制转换,错误可能延迟到运行期。
使用泛型后:
List<String> names = new ArrayList<>();
names.add("Ada");
// names.add(42); // 编译错误
String name = names.get(0);
List<String> 表达了一个编译期约束:通过这个引用可观察到的元素必须是 String。编译器会在添加、读取、传递参数等位置检查该约束。
泛型的核心收益不是“让容器记住类型”,而是把一部分运行期错误提前转化为编译期错误,同时减少显式强制转换。
List<String> names = new ArrayList<>();
names.add("Ada");
Object value = names.get(0); // 可以向上转型为 Object
String name = names.get(0); // 不需要手动转换
这里的 String 是类型参数,List<String> 是参数化类型,List 是泛型类型声明中的原始类型(raw type)。
2. 泛型声明的基本结构
2.1 泛型类
public final class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
T 是类型变量。使用时必须为它提供一个具体类型:
Box<String> text = new Box<>("hello");
String value = text.get();
Box<Integer> number = new Box<>(42);
Integer n = number.get();
同一个泛型类可以被不同类型参数化,但每个参数化类型都必须满足声明时的边界约束。
2.2 泛型接口和泛型方法
public interface Repository<ID, E> {
E findById(ID id);
void save(E entity);
}
泛型方法的类型参数写在返回类型前:
public static <T> T first(List<T> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("values is empty");
}
return values.get(0);
}
调用时通常不需要显式指定 T:
String first = first(List.of("a", "b"));
Integer number = first(List.of(1, 2));
编译器会根据方法参数、目标类型以及上下文推断类型参数。必要时可以显式提供类型见证(type witness):
String first = GenericExamples.<String>first(List.of("a", "b"));
泛型方法的类型参数属于该方法本身,不属于类的静态或实例状态。
2.3 <> 菱形语法
左侧已经提供了目标类型时,可以省略构造器中的类型参数:
List<String> names = new ArrayList<>();
Map<String, Integer> counts = new HashMap<>();
<> 不是“运行期创建泛型对象”的特殊机制,而是让编译器使用上下文进行类型推断。类型推断失败时,应显式写出类型参数:
var names = new ArrayList<String>();
对于局部变量,使用 var 时不要依赖模糊的上下文推断;显式写出 String 可以直接表达变量的真实静态类型。
3. 参数化类型不是子类型:泛型的类型不变性
Java 泛型默认是不变的(invariant)。
即使:
String 是 Object 的子类型
也不能推出:
List<String> 是 List<Object> 的子类型
因此下面的代码不能编译:
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误
如果允许这种赋值,就会破坏类型安全:
List<String> strings = new ArrayList<>();
// 假设 List<String> 可以赋值给 List<Object>
List<Object> objects = strings;
objects.add(42);
// 此时 strings 中出现了 Integer,
// 但它的静态类型承诺只能读取 String。
String value = strings.get(0);
数组则不同。数组是协变的:
String[] strings = new String[1];
Object[] objects = strings;
objects[0] = 42; // 运行期抛出 ArrayStoreException
数组把部分类型检查延迟到了运行期,而泛型通常在编译期拒绝这种不安全的赋值。这是数组协变与泛型不变的重要差异。
4. 有界类型参数:把类型能力表达出来
无界类型参数只能被当作 Object 使用:
public static <T> int sizeOf(T value) {
return value.toString().length();
}
如果方法需要调用某个上界中的方法,就要声明上界。
4.1 单个上界
public static <T extends Number> double twice(T value) {
return value.doubleValue() * 2;
}
T extends Number 表示:
T 必须是 Number 或 Number 的某个子类型
因此 doubleValue() 对 T 一定可用。
twice(10); // Integer
twice(3.14); // Double
// twice("x"); // 编译错误
泛型边界不仅能限制调用者,也决定了泛型方法内部可使用的操作。
4.2 多重上界
类型参数可以同时具有一个类上界和多个接口上界:
public static <T extends Number & Comparable<T>>
int compareToZero(T value) {
return value.compareTo((T) Integer.valueOf(0));
}
不过上面的示例存在不必要且不安全的转换。更合理的例子是:
public static <T extends Number & Comparable<? super T>>
int compare(T left, T right) {
return left.compareTo(right);
}
边界顺序要求类(如果有)必须写在最前面:
<T extends Number & Comparable<T> & Serializable>
不能写成:
// <T extends Comparable<T> & Number> // 编译错误
多个上界表示交集,而不是“满足其中任意一个”:
T ∈ Number ∩ Comparable<? super T> ∩ Serializable
4.3 递归类型边界
Java 常见的递归边界(F-bounded polymorphism)是:
public interface Comparable<T> {
int compareTo(T other);
}
一个类型实现:
public final class UserId implements Comparable<UserId> {
private final long value;
public UserId(long value) {
this.value = value;
}
@Override
public int compareTo(UserId other) {
return Long.compare(value, other.value);
}
}
方法可以要求类型与自身相关:
public static <T extends Comparable<? super T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
这里的 Comparable<? super T> 比 Comparable<T> 更宽。它允许某个父类型负责比较 T,例如某些类型由 Comparable<Object> 实现比较。
5. 通配符:表达未知但受约束的类型
类型参数和通配符的语义不同:
List<T>
List<?>
List<? extends Number>
List<? super Integer>
类型参数 T 通常表示“方法需要在多个位置使用同一个具体类型”;通配符表示“存在某个类型,但调用方不需要知道它的具体名称”。
5.1 无界通配符 <?>
public static void printAll(List<?> values) {
for (Object value : values) {
System.out.println(value);
}
}
List<?> 可以接收任何参数化的 List:
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));
但是不能向其中添加任意非 null 元素:
List<?> values = new ArrayList<String>();
// values.add("x"); // 编译错误
values.add(null); // 合法
原因是 List<?> 的真实元素类型可能是任意类型,例如 Integer、String 或 User。编译器无法证明 "x" 对所有可能类型都安全;但 null 可以赋给任何引用类型。
从读取角度看,List<?> 中的元素至少是 Object:
Object value = values.get(0);
<?> 适合表示“只需要读取对象,且不关心元素具体类型”。
5.2 上界通配符 <? extends T>
List<? extends Number> numbers;
它表示某个未知类型 X,且:
X extends Number
因此实际对象可能是:
List<Integer>
List<Double>
List<BigDecimal>
读取时可以安全地视为 Number:
public static double sum(List<? extends Number> numbers) {
double result = 0;
for (Number number : numbers) {
result += number.doubleValue();
}
return result;
}
但不能添加 Integer:
List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(1); // 编译错误
因为如果实际类型是 List<Double>,添加 Integer 就不安全。
extends 通配符的安全规则可以形式化为:
如果 X <: T,则 List<X> 可视为 List<? extends T>
读取 List<? extends T> 的结果可安全赋给 T
写入任意具体的 T 不安全
其中 X <: T 表示 X 是 T 的子类型。
5.3 下界通配符 <? super T>
List<? super Integer> values;
它表示某个未知类型 Y,且:
Integer <: Y
实际类型可能是:
List<Integer>
List<Number>
List<Object>
因此向其中写入 Integer 是安全的:
public static void addDefaults(List<? super Integer> values) {
values.add(0);
values.add(1);
}
但读取时只能安全地得到 Object:
Object value = values.get(0);
// Integer number = values.get(0); // 编译错误
原因是实际类型可能是 List<Object>,编译器无法证明读取结果一定是 Integer。
下界通配符的安全规则为:
如果 T <: Y,则 List<Y> 可视为 List<? super T>
向 List<? super T> 写入 T 或 T 的子类型是安全的
读取结果只能安全地视为 Object
6. PECS:生产者 extends,消费者 super
PECS 是对通配符读写规则的简写:
- Producer Extends:生产数据给你读取,用
extends; - Consumer Super:消费你写入的数据,用
super。
6.1 生产者
public static double average(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total / numbers.size();
}
这个方法从列表读取 Number,列表是生产者,因此使用 ? extends Number。
6.2 消费者
public static void copyIntegers(
List<? extends Integer> source,
List<? super Integer> destination) {
for (Integer value : source) {
destination.add(value);
}
}
这里:
source负责产生Integer,使用extends;destination负责接收Integer,使用super。
标准库中的 Collections.copy、Comparator.comparing 等 API 都大量使用这种思想。
6.3 为什么不能机械套用 PECS
PECS 是读写能力的推导结果,不是必须套用的命名规则。
如果方法需要在多个位置保持同一个类型关系,类型参数更合适:
public static <T> T chooseFirst(T first, T second) {
return first;
}
这里返回值和参数共享同一个 T。如果写成:
public static Object chooseFirst(Object first, Object second)
就丢失了调用方可以利用的精确类型。
另一个例子:
public static <T> void swap(List<T> list, int i, int j) {
T temporary = list.get(i);
list.set(i, list.get(j));
list.set(j, temporary);
}
swap 必须保证取出和写回的是同一个元素类型,所以使用 <T>,而不是简单使用 List<?>。虽然借助捕获转换,某些 List<?> 的内部操作也可以实现,但显式类型参数更准确地表达了方法契约。
7. 通配符捕获
编译器会把通配符看成一个捕获的未知类型。
例如:
public static void reverse(List<?> list) {
reverseCaptured(list);
}
private static <T> void reverseCaptured(List<T> list) {
int left = 0;
int right = list.size() - 1;
while (left < right) {
T temporary = list.get(left);
list.set(left, list.get(right));
list.set(right, temporary);
left++;
right--;
}
}
调用 reverseCaptured(list) 时,编译器为 ? 捕获一个具体但未知的类型 T。方法不需要知道 T 是 String 还是 Integer,只需要保证两次读取和写回使用同一个 T。
这解释了一个常见现象:
List<?> list = new ArrayList<String>();
// list.set(0, list.get(0)); // 这种表达式在某些上下文中可被捕获转换处理
对于复杂表达式,显式提取到泛型辅助方法通常可以让编译器更清楚地建立类型关系。
8. 类型擦除:泛型如何进入运行期
Java 泛型主要是编译期机制。编译器会进行类型检查,然后对泛型进行类型擦除(type erasure)。
擦除规则可以概括为:
- 无界类型变量擦除为
Object; - 有界类型变量擦除为其第一个上界;
- 参数化类型擦除为原始类型;
- 编译器在必要位置插入类型转换;
- 编译器可能生成桥接方法(bridge method)维持多态行为。
例如:
public final class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
如果 T 无界,擦除后概念上接近:
public final class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
调用方:
Box<String> box = new Box<>("hello");
String value = box.get();
编译器会在读取位置插入类似下面的转换:
String value = (String) box.get();
这不是说源代码被简单替换成文本,而是说明类型检查和字节码层面的核心效果。
8.1 有界类型的擦除
class NumericBox<T extends Number> {
private T value;
public T get() {
return value;
}
}
这里 T 的第一个上界是 Number,所以擦除后的字段和方法签名会以 Number 为基础,而不是 Object。
多个边界中,第一个上界尤其重要:
<T extends Number & Comparable<T>>
擦除基础是 Number,接口边界不会成为擦除后的第一个类型。
8.2 同一个泛型类不能仅按类型参数重载
下面的两个方法在擦除后都会接近 process(List):
// void process(List<String> values)
// void process(List<Integer> values)
因此会产生名称冲突,不能通过类型参数区分重载:
public void process(List<String> values) { }
// public void process(List<Integer> values) { } // 编译错误
方法返回值也不能单独用于重载区分。
8.3 泛型继承与桥接方法
interface Converter<T> {
T convert(T input);
}
final class StringConverter implements Converter<String> {
@Override
public String convert(String input) {
return input.trim();
}
}
擦除后,接口方法基础签名类似:
Object convert(Object input);
但 StringConverter 的源代码方法是:
String convert(String input);
为了保证接口多态调用仍然成立,编译器通常会生成一个桥接方法,概念上类似:
public Object convert(Object input) {
return convert((String) input);
}
桥接方法是编译器生成的实现细节,但反射可以观察到它:
for (var method : StringConverter.class.getDeclaredMethods()) {
System.out.println(
method.getName()
+ ", bridge=" + method.isBridge()
+ ", synthetic=" + method.isSynthetic());
}
实际输出可能包含源方法和桥接方法。反射框架如果需要处理业务方法,通常应考虑过滤 isBridge() 和 isSynthetic(),否则可能重复发现同一个逻辑操作。
9. 可具体化类型与运行期限制
可具体化类型(reifiable type)是指运行期仍能完整表达其类型信息的类型。典型包括:
- 非泛型类型,例如
String; - 原始类型,例如
List; - 无界通配符参数化类型,例如
List<?>; - 基本类型;
- 元素类型可具体化的数组,例如
String[]、List<?>[]。
参数化类型通常不可具体化:
List<String> // 运行期通常只知道是 List
因此不能写:
// if (value instanceof List<String>) { } // 编译错误
可以写:
if (value instanceof List<?> list) {
System.out.println(list.size());
}
也不能直接创建类型变量数组:
// T[] values = new T[10]; // 编译错误
因为运行期无法确定 T 的实际数组组件类型。通常可以传入数组构造器:
import java.util.function.IntFunction;
public static <T> T[] copyOf(
T[] source,
IntFunction<T[]> arrayFactory) {
T[] result = arrayFactory.apply(source.length);
System.arraycopy(source, 0, result, 0, source.length);
return result;
}
调用:
String[] source = {"a", "b"};
String[] copy = copyOf(source, String[]::new);
这里由调用方提供运行期数组类型信息。
10. 堆污染、原始类型与 @SafeVarargs
堆污染(heap pollution)是指某个参数化类型的变量引用了不符合其参数化类型约束的对象。最常见来源是原始类型和不安全的泛型可变参数。
10.1 原始类型
List<String> names = new ArrayList<>();
List raw = names;
raw.add(42); // 编译警告
从 names 读取时:
String name = names.get(0); // 可能抛出 ClassCastException
原始类型保留主要是为了兼容 Java 5 之前的旧代码。新代码应使用参数化类型或通配符。
如果确实需要处理任意参数化的列表:
List<?> values
通常优于:
List values
因为 List<?> 仍然保留了“这是一个参数化列表”的类型安全约束。
10.2 泛型可变参数
数组和泛型结合时可能产生堆污染:
@SafeVarargs
static void printLists(List<String>... lists) {
for (List<String> list : lists) {
System.out.println(list);
}
}
编译器会对非具体化类型的可变参数发出警告,因为内部实际使用的是类似 List[] 的数组,而数组运行期无法区分 List<String> 和 List<Integer>。
@SafeVarargs 不是关闭所有警告的装饰,而是调用者对方法实现作出的安全承诺。只有当方法不会把可变参数数组暴露出去、不会向其中写入不兼容的数组元素、不会通过不安全方式使用它时,才应该使用它。
该注解可用于:
static方法;final实例方法;- 构造器。
普通可覆写实例方法不能使用 @SafeVarargs,因为子类实现可能破坏安全承诺。
11. 泛型的其他编译期限制
11.1 不能直接实例化类型变量
class Factory<T> {
// T create() { return new T(); } // 编译错误
}
运行期擦除后,T 可能对应任意类型,编译器没有可调用的构造器信息。
可改为传入 Supplier<T>:
import java.util.function.Supplier;
class Factory<T> {
private final Supplier<? extends T> creator;
Factory(Supplier<? extends T> creator) {
this.creator = creator;
}
T create() {
return creator.get();
}
}
Factory<String> factory = new Factory<>(String::new);
11.2 静态成员不能使用类的类型变量
class Holder<T> {
// static T value; // 编译错误
}
Holder<String> 和 Holder<Integer> 共享同一个 Holder.class,不存在分别属于两种参数化类型的静态字段。
但静态方法可以声明自己的类型参数:
class Functions {
public static <T> T identity(T value) {
return value;
}
}
11.3 不能对参数化类型使用精确的 instanceof
// value instanceof List<String> // 编译错误
value instanceof List<?> // 合法
需要先检查原始容器,再逐个检查元素:
static boolean isStringList(Object value) {
if (!(value instanceof List<?> list)) {
return false;
}
for (Object element : list) {
if (!(element instanceof String)) {
return false;
}
}
return true;
}
这段检查只能证明当前元素都为 String,不能改变对象运行期已经擦除的实际类信息。
12. 泛型数组与数组协变的冲突
下面的代码不合法:
// List<String>[] array = new List<String>[10]; // 编译错误
如果允许创建,数组协变会产生问题:
List<String>[] strings = ...;
Object[] objects = strings;
objects[0] = List.of(42);
数组运行期只能检查数组组件类型是 List,无法检查参数化类型是 List<String>,因此无法保证类型安全。
可以创建通配符数组:
List<?>[] values = new List<?>[10];
values[0] = List.of("a");
values[1] = List.of(1);
因为 List<?> 是可具体化类型。它表达的是“数组元素都是某种参数化列表,但每个列表的参数类型可能不同”。
13. 反射中的泛型:运行期能看到什么
类型擦除并不意味着所有泛型信息都消失。编译器通常会把声明位置的泛型信息写入 class 文件的 Signature 属性。反射 API 可以读取这些声明信息。
但是要区分两件事:
声明信息:类、字段、方法声明时写了什么泛型
对象实例:这个对象运行期实际装了什么类型
前者通常可以通过反射读取;后者通常不能从对象的 Class 直接恢复。
13.1 Class<T>
Class<T> 是一个参数化的反射类型:
Class<String> stringClass = String.class;
String value = stringClass.cast("hello");
Class.cast 会在运行期进行类型检查,比直接强制转换更适合封装反射逻辑。
泛型类的类字面量不能携带类型参数:
Class<List> rawListClass = List.class;
Class<?> anyClass = List.class;
不存在:
// List<String>.class // 编译错误
如果 API 要表示 List<String> 这样的运行期类型,通常需要额外的类型令牌(type token)或框架自定义的 Type 封装。
13.2 Type 及其主要子接口
java.lang.reflect.Type 是反射泛型类型信息的总接口。常见实现包括:
Class<?>:普通类、接口、数组、基本类型;ParameterizedType:如List<String>;TypeVariable<?>:如声明中的T;WildcardType:如? extends Number、? super Integer;GenericArrayType:如T[]或List<String>[]这类非具体化泛型数组类型。
getType() 和 getGenericType() 的区别很重要:
import java.lang.reflect.Field;
Field field = SomeClass.class.getDeclaredField("values");
Class<?> erasedType = field.getType();
Type genericType = field.getGenericType();
如果字段声明为:
List<String> values;
那么:
getType() -> interface java.util.List
getGenericType() -> java.util.List<java.lang.String>
前者是擦除后的 Class,后者保留声明中的泛型结构。
14. 可运行的反射泛型示例
下面的程序演示:
- 从泛型父类读取实际类型参数;
- 从字段读取
ParameterizedType; - 从方法参数读取
TypeVariable; - 递归打印
Type结构。
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;
import java.util.Map;
class Repository<E> {
public E findById(Object id) {
return null;
}
}
final class UserRepository extends Repository<User> {
private List<String> tags;
private Map<String, Integer> scores;
public void save(User user) {
// 示例方法
}
}
record User(long id, String name) {
}
public class GenericReflectionDemo {
public static void main(String[] args) throws Exception {
inspectGenericSuperclass(UserRepository.class);
inspectFields(UserRepository.class);
inspectMethod(UserRepository.class.getMethod("save", User.class));
inspectTypeVariable(Repository.class);
}
private static void inspectGenericSuperclass(Class<?> type) {
Type genericSuperclass = type.getGenericSuperclass();
System.out.println("generic superclass: " + genericSuperclass);
if (genericSuperclass instanceof ParameterizedType parameterized) {
System.out.println("raw type: " + parameterized.getRawType());
for (Type argument : parameterized.getActualTypeArguments()) {
System.out.println("actual argument: " + argument);
}
}
}
private static void inspectFields(Class<?> type) {
for (Field field : type.getDeclaredFields()) {
System.out.println(
"field " + field.getName()
+ ", erased=" + field.getType().getTypeName()
+ ", generic=" + field.getGenericType());
printType(" ", field.getGenericType());
}
}
private static void inspectMethod(Method method) {
System.out.println("method: " + method.getName());
for (Type parameterType : method.getGenericParameterTypes()) {
System.out.println("parameter: " + parameterType);
printType(" ", parameterType);
}
}
private static void inspectTypeVariable(Class<?> type) {
for (TypeVariable<?> variable : type.getTypeParameters()) {
System.out.println(
"type variable: " + variable.getName()
+ ", declaration=" + variable.getGenericDeclaration());
for (Type bound : variable.getBounds()) {
System.out.println(" bound: " + bound);
}
}
}
private static void printType(String indent, Type type) {
if (type instanceof Class<?> clazz) {
System.out.println(indent + "Class: " + clazz.getTypeName());
} else if (type instanceof ParameterizedType parameterized) {
System.out.println(indent + "ParameterizedType: " + parameterized);
System.out.println(indent + "raw:");
printType(indent + " ", parameterized.getRawType());
System.out.println(indent + "arguments:");
for (Type argument : parameterized.getActualTypeArguments()) {
printType(indent + " ", argument);
}
} else if (type instanceof TypeVariable<?> variable) {
System.out.println(indent + "TypeVariable: " + variable.getName());
} else {
System.out.println(indent + "Other Type: " + type);
}
}
}
典型输出的关键部分类似:
generic superclass: Repository<User>
raw type: class Repository
actual argument: class User
field tags, erased=java.util.List, generic=java.util.List<java.lang.String>
field scores, erased=java.util.Map, generic=java.util.Map<java.lang.String, java.lang.Integer>
method: save
parameter: class User
type variable: E, declaration=class Repository
bound: class java.lang.Object
输出中的 Repository<User> 来自类声明的签名元数据,而不是来自 Repository 实例在运行期保存了一个 User.class 字段。
14.1 反射读取泛型父类的实际参数
Type type = UserRepository.class.getGenericSuperclass();
返回的是 Type,因为父类可能是:
- 普通类;
- 参数化类型;
- 类型变量;
- 其他合法的泛型结构。
只有确认它是 ParameterizedType 后,才能调用:
getRawType()
getActualTypeArguments()
不能无条件强制转换:
// ParameterizedType type =
// (ParameterizedType) SomeClass.class.getGenericSuperclass();
如果 SomeClass 直接继承普通父类,这样会抛出 ClassCastException。
14.2 反射读取字段泛型
Field field = UserRepository.class.getDeclaredField("tags");
Type erased = field.getType();
Type declared = field.getGenericType();
erased 是 List.class,declared 是 List<String> 对应的 ParameterizedType。
如果字段是:
private List<E> values;
那么参数类型可能是 TypeVariable;如果字段是:
private List<? extends Number> values;
那么参数类型会包含 WildcardType。
因此生产级泛型解析器不能只处理 Class 和 ParameterizedType,还必须处理类型变量、通配符以及泛型数组。
15. 反射不能自动知道实例的实际泛型参数
考虑:
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
运行期两者通常都是:
java.util.ArrayList
下面的结果相同:
strings.getClass() == integers.getClass()
因为类型参数参与编译期检查,而不是参与普通对象的运行期类身份。
如果需要保留 List<String> 这类类型信息,必须显式保存声明信息或传入类型令牌。例如:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
Type superclass = getClass().getGenericSuperclass();
if (!(superclass instanceof ParameterizedType parameterized)) {
throw new IllegalStateException(
"TypeToken must be created with an anonymous subclass");
}
this.type = parameterized.getActualTypeArguments()[0];
}
public Type type() {
return type;
}
}
public class TypeTokenDemo {
public static void main(String[] args) {
TypeToken<java.util.List<String>> token =
new TypeToken<>() { };
System.out.println(token.type());
}
}
输出通常为:
java.util.List<java.lang.String>
这里保存信息的关键不是 List 对象本身,而是匿名子类的泛型父类声明:
TypeToken<List<String>>
这种技术依赖声明签名反射,不能用于恢复任意已有对象的运行期实际元素类型。
16. 反射调用泛型方法时的类型边界
反射调用使用的是擦除后的运行期方法签名,但方法对象本身仍可通过泛型声明 API 读取类型信息:
Method method = Repository.class.getMethod("findById", Object.class);
System.out.println(method.getReturnType());
System.out.println(method.getGenericReturnType());
对于:
class Repository<E> {
public E findById(Object id) {
return null;
}
}
结果大致为:
class java.lang.Object
E
getReturnType() 体现擦除后的返回类型;getGenericReturnType() 体现声明中的类型变量。
反射不会替代类型检查。例如:
Method method = SomeClass.class.getMethod("setName", String.class);
method.invoke(target, 123);
调用参数类型不匹配时,反射调用会失败,常见表现是 IllegalArgumentException。如果目标方法自身抛出异常,Method.invoke 通常会以 InvocationTargetException 包装该异常,诊断时应检查其 getCause()。
泛型反射解析器还要处理类型替换。例如:
class Parent<T> {
T value;
}
class Child extends Parent<String> {
}
直接检查 Parent.class 的字段只能看到 T;要推导 Child.value 的最终类型是 String,必须沿继承关系建立:
T -> String
再将这个映射应用到字段、方法返回值和参数类型。仅调用一次 getGenericType() 并不能自动完成这种类型变量替换。
17. 泛型与集合 API 的完整示例
下面的方法展示一个常见的数据流:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class CollectionFunctions {
private CollectionFunctions() {
}
public static <T> void appendAll(
Collection<? super T> destination,
Iterable<? extends T> source) {
for (T element : source) {
destination.add(element);
}
}
public static <T extends Comparable<? super T>>
T maximum(Iterable<? extends T> values) {
var iterator = values.iterator();
if (!iterator.hasNext()) {
throw new IllegalArgumentException("values must not be empty");
}
T result = iterator.next();
while (iterator.hasNext()) {
T candidate = iterator.next();
if (candidate.compareTo(result) > 0) {
result = candidate;
}
}
return result;
}
public static void main(String[] args) {
List<Integer> source = List.of(3, 1, 7);
List<Number> destination = new ArrayList<>();
appendAll(destination, source);
System.out.println(destination);
System.out.println(maximum(source));
}
}
执行过程如下:
source的元素类型是Integer;Iterable<? extends T>允许它作为T的生产者;destination是List<Number>;Collection<? super T>允许它作为T的消费者;- 类型推断选择
T = Integer; Integer可以添加到Collection<Number>;Integer实现了Comparable<Integer>,满足Comparable<? super Integer>;maximum返回Integer。
预期输出:
[3, 1, 7]
7
如果把目标列表改成:
List<String> destination = new ArrayList<>();
调用会在编译期失败,因为 String 不是 Integer 的父类型,不能满足 Collection<? super Integer>。
18. 常见误解与失败表现
18.1 “List<Object> 可以接收所有列表”
错误。List<Object> 表示可以安全写入任意 Object,而不是“一个任意元素类型的列表”。
如果方法只需要读取任意列表,应使用:
List<?>
如果方法需要写入任意对象,应要求真正的:
List<Object>
这两者语义不同。
18.2 “extends 表示可以向列表添加子类”
错误。List<? extends Number> 的类型变量是未知的具体子类型。正因为不知道它是 Integer 还是 Double,所以不能添加任意 Number。
List<? extends Number> values = List.of(1, 2);
// values.add(3); // 不允许
它的重点是安全读取,而不是允许写入。
18.3 “反射一定能得到泛型参数”
错误。反射可以读取类、字段和方法声明中保留的泛型签名,但不能从普通 ArrayList 实例中判断它曾经被声明为 ArrayList<String> 还是 ArrayList<Integer>。
18.4 “类型擦除意味着运行期完全没有泛型信息”
也不准确。类文件可能保留 Signature 属性,getGenericSuperclass()、getGenericReturnType() 等 API 可以读取它。但这些信息描述的是声明,且可能包含未解析的 TypeVariable。
18.5 “使用原始类型只是少写几个字符”
原始类型会关闭一部分泛型检查:
List raw = new ArrayList<String>();
raw.add(42);
风险不会消失,只会从编译期推迟到读取时的类型转换位置,最终可能表现为:
ClassCastException
18.6 “所有泛型警告都可以用 @SuppressWarnings 消除”
抑制警告不会修复堆污染。正确流程是:
- 定位警告来源;
- 判断是否能通过通配符、类型参数或 API 重构消除;
- 如果必须进行未经检查的转换,缩小其范围;
- 通过运行期校验或不变量证明其安全;
- 只在最小代码范围内抑制警告,并说明原因。
例如:
@SuppressWarnings("unchecked")
static <T> T uncheckedCast(Object value) {
return (T) value;
}
这段代码没有任何安全证明,只是把错误推迟了。生产代码中必须让调用方、边界校验或封装协议提供实际保证。
19. 泛型 API 的设计边界
一个泛型 API 的参数应由它真实执行的操作决定。
只读:
void print(List<?> values)
读取为某个父类型:
double sum(List<? extends Number> values)
写入某个类型:
void addDefaults(List<? super Integer> values)
需要关联多个位置的同一类型:
<T> T first(List<T> values)
需要类型能力:
<T extends Comparable<? super T>> T max(List<? extends T> values)
泛型越精确,编译器能验证的关系越多;但过度复杂的通配符会增加调用者和实现者的理解成本。边界应表达方法实际依赖的能力,而不是为了“看起来泛型化”而添加。
20. 与 Java 25 的关系
Java 25 中泛型的基本语义仍由 Java 语言规范定义:
- 泛型主要进行编译期类型检查;
- 参数化类型默认不变;
- 类型擦除保持与旧版本 Java 的二进制兼容模型;
- 通配符表达未知类型及其上界或下界;
- 反射通过
Type体系访问声明中的泛型信息。
Java 版本升级不会把普通泛型集合变成运行期携带完整元素类型的容器,也不会让 List<String>.class 这类语法成立。使用 Java 25 时,仍应根据编译器检查、字节码桥接方法、类文件签名和反射 Type 结构来理解泛型的实际边界。
泛型的完整心智模型可以归纳为:
类型参数:建立多个位置之间的同一类型关系
上界:限制类型必须具备的能力
通配符:隐藏具体类型,同时暴露安全的读写方向
extends:安全生产,主要用于读取
super:安全消费,主要用于写入
类型擦除:保证泛型与旧版 JVM 类型系统兼容
反射 Type:读取声明时保留的泛型结构,而非恢复任意对象的实际类型参数
理解这些关系后,List<? extends Number> 为什么不能写入、List<? super Integer> 为什么只能读成 Object、泛型数组为什么受限,以及反射为什么有时得到 String、有时得到 T,都可以从同一套类型规则中推导出来。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java 集合框架:List、Set、Map、Queue、迭代器和复杂度
- 下一篇:Java 异常处理:受检异常、错误链、资源关闭和 API 契约
- 延伸:Java 反射与注解:Class、MethodHandle、元注解、处理器和边界
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论