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);

这里有两个问题:

  1. List 可以混入任何引用类型;
  2. 取值时必须手动强制转换,错误可能延迟到运行期。

使用泛型后:

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<?> 的真实元素类型可能是任意类型,例如 IntegerStringUser。编译器无法证明 "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 表示 XT 的子类型。

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.copyComparator.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。方法不需要知道 TString 还是 Integer,只需要保证两次读取和写回使用同一个 T

这解释了一个常见现象:

List<?> list = new ArrayList<String>();
// list.set(0, list.get(0)); // 这种表达式在某些上下文中可被捕获转换处理

对于复杂表达式,显式提取到泛型辅助方法通常可以让编译器更清楚地建立类型关系。


8. 类型擦除:泛型如何进入运行期

Java 泛型主要是编译期机制。编译器会进行类型检查,然后对泛型进行类型擦除(type erasure)。

擦除规则可以概括为:

  1. 无界类型变量擦除为 Object
  2. 有界类型变量擦除为其第一个上界;
  3. 参数化类型擦除为原始类型;
  4. 编译器在必要位置插入类型转换;
  5. 编译器可能生成桥接方法(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();

erasedList.classdeclaredList<String> 对应的 ParameterizedType

如果字段是:

private List<E> values;

那么参数类型可能是 TypeVariable;如果字段是:

private List<? extends Number> values;

那么参数类型会包含 WildcardType

因此生产级泛型解析器不能只处理 ClassParameterizedType,还必须处理类型变量、通配符以及泛型数组。


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));
    }
}

执行过程如下:

  1. source 的元素类型是 Integer
  2. Iterable<? extends T> 允许它作为 T 的生产者;
  3. destinationList<Number>
  4. Collection<? super T> 允许它作为 T 的消费者;
  5. 类型推断选择 T = Integer
  6. Integer 可以添加到 Collection<Number>
  7. Integer 实现了 Comparable<Integer>,满足 Comparable<? super Integer>
  8. 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 消除”

抑制警告不会修复堆污染。正确流程是:

  1. 定位警告来源;
  2. 判断是否能通过通配符、类型参数或 API 重构消除;
  3. 如果必须进行未经检查的转换,缩小其范围;
  4. 通过运行期校验或不变量证明其安全;
  5. 只在最小代码范围内抑制警告,并说明原因。

例如:

@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、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。