8. 泛型程序设计

学习路线:

  1. 为什么要有泛型

  2. 泛型类:class Box

  3. 泛型方法:public static T getMiddle(T... values)

  4. 类型变量的限定:<T extends Comparable>

  5. 泛型代码和虚拟机:类型擦除

  6. 泛型的限制:不能 new T()、不能创建泛型数组等

  7. 通配符:? extends 和 ? super

  8. 反射与泛型

0. 为什么要有泛型

没有泛型时:

java 复制代码
ArrayList list = new ArrayList();
list.add("Alice");

String name = (String) list.get(0);

问题是:

  • 取元素时需要强制类型转换。
  • 如果放错类型,编译器不一定能发现。
  • 错误可能拖到运行期才爆发。

有了泛型:

java 复制代码
ArrayList<String> list = new ArrayList<>();
list.add("Alice");

String name = list.get(0);

这时:

  • list 只能放 String
  • get 出来的结果自动就是 String
  • 编译器会帮你检查类型错误

如果你写:

java 复制代码
list.add(123);

编译器直接报错。

1. 泛型类

最简单的泛型类

java 复制代码
public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

这里的 T 是类型参数。创建对象时再决定它具体是什么类型:

java 复制代码
Box<String> stringBox = new Box<>();
stringBox.set("hello");

String s = stringBox.get();

泛型类可以有多个类型参数

java 复制代码
public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

使用:

java 复制代码
Pair<String, Integer> score = new Pair<>("Alice", 95);

String name = score.getKey();
Integer points = score.getValue();

2. 泛型方法

泛型方法是方法自己带类型参数:

java 复制代码
public static <T> T getFirst(T[] array) {
    return array[0];
}

调用时:

java 复制代码
String[] names = {"Alice", "Bob"};
String firstName = getFirst(names);

Integer[] numbers = {1, 2, 3};
Integer firstNumber = getFirst(numbers);

注意这里的 写在返回类型前面:

java 复制代码
public static <T> T getFirst(T[] array)

第一个 是声明类型参数,后面的 T 才是返回类型。如果没有前面的 ,编译器不知道 T 是什么。

泛型方法不一定在泛型类里

这是一个容易误解的地方。

普通类里也可以有泛型方法:

java 复制代码
public class ArrayUtil {
    public static <T> T getFirst(T[] array) {
        return array[0];
    }
}

ArrayUtil 本身不是泛型类,但 getFirst 是泛型方法。

反过来,泛型类里的普通方法不一定是泛型方法:

java 复制代码
public class Box<T> {
    private T value;

    public T get() {
        return value;
    }
}

这里 get() 使用了类上的 T,但它自己没有声明新的类型参数,所以它不是"泛型方法",而是"泛型类中的普通方法"。

3. 类型变量的限定

有时候,泛型不能太自由。

比如你要写一个方法,找出数组中的最小值:

java 复制代码
public static <T> T min(T[] values) {
    // 怎么比较两个 T?
}

问题来了:不是所有对象都能比较大小。

所以要限制 T 必须实现 Comparable:

java 复制代码
public static <T extends Comparable<T>> T min(T[] values) {
    T smallest = values[0];

    for (T value : values) {
        if (value.compareTo(smallest) < 0) {
            smallest = value;
        }
    }

    return smallest;
}

这里:

java 复制代码
<T extends Comparable<T>>

意思是:T 必须是实现了 Comparable 的类型。

比如 String、Integer 都可以比较,所以能用:

java 复制代码
String[] words = {"apple", "orange", "banana"};
String minWord = min(words);

为什么用 extends,不用 implements

哪怕后面跟的是接口,也写 extends:

java 复制代码
<T extends Comparable<T>>

而不是:

java 复制代码
<T implements Comparable<T>> // 错

在泛型限定里,extends 统一表示"是某个类型的子类型":

多个限定

一个类型变量可以有多个限定:

java 复制代码
<T extends Comparable<T> & Serializable>

意思是:T 必须既能比较,又能序列化

如果同时有类和接口,类必须放在第一个:

java 复制代码
<T extends Employee & Comparable<T> & Serializable>

不能写成:

java 复制代码
<T extends Comparable<T> & Employee> // 错

因为 Java 的泛型擦除机制需要用第一个限定作为主要擦除类型。这个先记结论就好,后面讲类型擦除时会更清楚

限定之后能做什么

没有限定时:

java 复制代码
public static <T> void test(T value) {
    value.compareTo(...); // 不行
}

因为 T 只能当成 Object 用。

有了限定:

java 复制代码
public static <T extends Comparable<T>> void test(T value, T other) {
    value.compareTo(other); // 可以
}

编译器知道 value 至少是一个 Comparable。

再比如:

java 复制代码
public static <T extends Number> double sum(T[] values) {
    double result = 0;

    for (T value : values) {
        result += value.doubleValue();
    }

    return result;
}

因为 T extends Number,所以可以调用:

java 复制代码
value.doubleValue()

限定不等于继承泛型类

这一点也容易混。

java 复制代码
class Box<T extends Number> {
    private T value;
}

这表示:Box 里面的 T 只能是 Number 或 Number 的子类

所以可以:

java 复制代码
Box<Integer> a;
Box<Double> b;
Box<Number> c;

不可以:

java 复制代码
Box<String> d; // 编译错误

但它不表示 Box 是 Box 的子类。

也就是说:

java 复制代码
Box<Integer> intBox = new Box<>();
Box<Number> numberBox = intBox; // 错

这是泛型里非常重要的一条:

即使 Integer 是 Number 的子类,Box 也不是 Box 的子类。

4. 类型擦除

类型擦除是什么

Java 的泛型主要存在于编译期。编译器会检查类型是否安全,但生成字节码时,会把很多泛型类型信息"擦掉"。

比如你写:

java 复制代码
public class Pair<T> {
    private T first;
    private T second;

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }
}

如果 T 没有限定,编译后大致会变成:

java 复制代码
public class Pair {
    private Object first;
    private Object second;

    public Object getFirst() {
        return first;
    }

    public void setFirst(Object first) {
        this.first = first;
    }
}

也就是说:T 被擦除成 Object

所以:

java 复制代码
Pair<String>
Pair<Integer>
Pair<Employee>

在运行时本质上都是同一个类:Pair

这就是为什么下面代码输出通常是 true:

java 复制代码
Pair<String> p1 = new Pair<>();
Pair<Integer> p2 = new Pair<>();

System.out.println(p1.getClass() == p2.getClass());

因为运行时只有一个 Pair.class。

4.1 有上界时擦除成上界

如果类型变量有限定:

java 复制代码
public class Interval<T extends Comparable<T>> {
    private T lower;
    private T upper;

    public T getLower() {
        return lower;
    }
}

T 不会擦除成 Object,而是擦除成它的第一个限定:

java 复制代码
public class Interval {
    private Comparable lower;
    private Comparable upper;

    public Comparable getLower() {
        return lower;
    }
}

如果是多个限定:

java 复制代码
<T extends Employee & Comparable<T> & Serializable>

擦除成第一个限定:Employee

这也是为什么多个限定中,类要放在最前面。

4.2 编译器会插入强制类型转换

你写:

java 复制代码
Pair<String> pair = new Pair<>();
String s = pair.getFirst();

擦除后 getFirst() 实际返回的是 Object。

那为什么能赋给 String?

因为编译器帮你插入了强制类型转换:

java 复制代码
String s = (String) pair.getFirst();

所以泛型不是让 JVM 真正生成一个 Pair 类,而是:

编译期检查类型安全

擦除泛型信息

必要时插入类型转换

4.3 桥方法

类型擦除还有一个比较经典的副作用:桥方法。

看这个例子:

java 复制代码
class Pair<T> {
    public void setSecond(T second) {
        // ...
    }
}

class DateInterval extends Pair<LocalDate> {
    @Override
    public void setSecond(LocalDate second) {
        // ...
    }
}

泛型擦除后,父类方法大致变成:

java 复制代码
public void setSecond(Object second)

子类方法是:

java 复制代码
public void setSecond(LocalDate second)

问题来了:这两个方法参数不同,按普通规则不算重写。

但从源码语义上,DateInterval 明明是在重写 Pair 的 setSecond。

为了保持多态,编译器会在子类里生成一个桥方法:

java 复制代码
public void setSecond(Object second) {
    setSecond((LocalDate) second);
}

这样通过父类引用调用时,多态仍然成立:

java 复制代码
Pair<LocalDate> pair = new DateInterval();
pair.setSecond(LocalDate.now());

桥方法是编译器自动生成的,平时你不用手写,但它解释了为什么类型擦除后泛型方法仍能保持多态。

4.4 不能在运行时判断具体泛型参数

因为类型擦除,所以不能这样:

java 复制代码
if (pair instanceof Pair<String>) {
    // 错
}

只能这样:

java 复制代码
if (pair instanceof Pair) {
    // 可以
}

同理,运行时也不能区分:

java 复制代码
ArrayList<String>
ArrayList<Integer>

它们都是:ArrayList

所以这类代码不可靠,也不允许

java 复制代码
list instanceof ArrayList<String>

不能 new T()

很多人初学泛型会想写:

java 复制代码
public class Box<T> {
    public T create() {
        return new T(); // 错
    }
}

为什么不行?

因为运行时 T 已经被擦除了。JVM 不知道你想创建的是 String、Employee 还是别的类型。

如果需要创建对象,通常要传入构造器、Class 或工厂:

java 复制代码
public class Box<T> {
    private Supplier<T> supplier;

    public Box(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}

使用:

java 复制代码
Box<Employee> box = new Box<>(Employee::new);
Employee e = box.create();

一句话总结类型擦除

Java 泛型是编译期的类型安全机制;运行时大多数泛型类型参数会被擦除成 Object 或上界类型。

5. 泛型的限制与局限性

这些限制看起来零散,但根源大多都是一个:类型擦除。也就是运行时 JVM 通常不知道 T 到底是什么。

  1. 不能用基本类型作为类型参数
    不能这样写:
java 复制代码
ArrayList<int> numbers = new ArrayList<>(); // 错

必须写包装类型:

java 复制代码
ArrayList<Integer> numbers = new ArrayList<>();

因为泛型类型参数必须是引用类型,不能是基本类型。

  1. 不能用 instanceof 检查参数化类型
    不能这样:
java 复制代码
if (list instanceof ArrayList<String>) {
    // 错
}

因为运行时 ArrayList 和 ArrayList 都被擦除成 ArrayList。

只能这样:

java 复制代码
if (list instanceof ArrayList) {
    // 可以
}

或者更推荐:

java 复制代码
if (list instanceof ArrayList<?>) {
    // 可以
}

ArrayList<?> 表示"某种元素类型的 ArrayList",但不关心具体是什么。

  1. 不能创建参数化类型数组
    不能这样:
java 复制代码
ArrayList<String>[] lists = new ArrayList<String>[10]; // 错

为什么?

数组在运行时会记住自己的元素类型,而泛型在运行时被擦除了。这两套机制放在一起容易破坏类型安全。

比如如果允许:

java 复制代码
ArrayList<String>[] lists = new ArrayList<String>[10];
Object[] objects = lists;
objects[0] = new ArrayList<Integer>();
String s = lists[0].get(0);

这里就可能把 ArrayList 塞进 ArrayList\[\] 里,最后取 String 时出问题。

所以 Java 干脆禁止创建参数化类型数组。

可以声明,但不能创建

java 复制代码
ArrayList<String>[] lists;              // 可以声明
lists = new ArrayList<String>[10];      // 不可以创建

实际开发中通常用集合替代数组:

java 复制代码
ArrayList<ArrayList<String>> lists = new ArrayList<>();
  1. 不能实例化类型变量
    不能这样:
java 复制代码
public class Box<T> {
    public T create() {
        return new T(); // 错
    }
}

因为运行时不知道 T 是什么。

常见替代方案是传入 Supplier:

java 复制代码
public class Box<T> {
    private final Supplier<T> supplier;

    public Box(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}

使用:

java 复制代码
Box<Employee> box = new Box<>(Employee::new);
Employee e = box.create();

或者传入 Class,用反射创建,不过现在更推荐工厂或 Supplier。

  1. 不能创建泛型数组
    不能这样:
java 复制代码
public class Stack<T> {
    private T[] elements = new T[10]; // 错
}

因为 T 被擦除,运行时不知道数组元素类型。

常见做法:用 Object\[\] 保存,取出时转换:

java 复制代码
public class Stack<T> {
    private Object[] elements = new Object[10];
    private int size = 0;

    public void push(T value) {
        elements[size++] = value;
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        return (T) elements[--size];
    }
}
  1. 泛型类的静态上下文不能使用类型变量 T
    不能这样:
java 复制代码
public class Box<T> {
    private static T value; // 错
}

也不能:

java 复制代码
public class Box<T> {
    public static T getValue() { // 错
        return null;
    }
}

原因是:T 属于某个具体的 Box 实例类型,而 static 属于整个类。

运行时只有一个 Box.class,不是:

java 复制代码
Box<String>.class
Box<Integer>.class

如果允许 static T value,那 Box 和 Box 到底共享一个什么类型的静态变量?说不清。

不过静态方法可以声明自己的类型参数:

java 复制代码
public class Box<T> {
    public static <U> U identity(U value) {
        return value;
    }
}

这里的 U 是静态方法自己的类型参数,和类的 T 无关。

  1. 不能抛出或捕获泛型类的异常
    不能让泛型类继承 Throwable:
java 复制代码
class Problem<T> extends Exception { // 错
}

也不能捕获类型变量:

java 复制代码
public static <T extends Throwable> void test() {
    try {
        // ...
    } catch (T e) { // 错
    }
}

因为异常捕获是在运行时按异常类型匹配的,而泛型类型参数会被擦除。

不过方法可以声明抛出类型变量:

java 复制代码
public static <T extends Throwable> void doWork(T exception) throws T {
    throw exception;
}

这个可以,常见于一些高级库代码。

  1. 擦除后不能造成方法冲突
    比如:
java 复制代码
public void print(List<String> list) {}

public void print(List<Integer> list) {}

这两个方法不能同时存在。

因为类型擦除后都变成:

java 复制代码
public void print(List list) {}

6. 泛型通? extends 与 ? super

也就是这些写法:

java 复制代码
List<?>
List<? extends Number>
List<? super Integer>

先抓住一个大方向:通配符是为了让泛型类型之间的关系更灵活。

因为 Java 泛型默认是不变的。

6.1 为什么需要通配符

我们知道:

java 复制代码
Integer 是 Number 的子类

但这不代表:

java 复制代码
List<Integer> 是 List<Number> 的子类

所以这段代码是错的:

java 复制代码
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers; // 错

为什么 Java 不允许?

如果允许,就会出问题:

java 复制代码
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;

numbers.add(3.14); // Double 也是 Number
Integer x = integers.get(0); // 这里就炸了

所以 Java 禁止 List 赋给 List。

但现实中,我们经常需要一个方法能接收各种 Number 子类列表:

java 复制代码
List<Integer>
List<Double>
List<BigDecimal>

这时就要用:

java 复制代码
List<? extends Number>

6.2 上界通配符:? extends

java 复制代码
List<? extends Number>

意思是:某种未知类型的 List,但这个未知类型一定是 Number 或 Number 的子类。

所以它可以接收:

java 复制代码
List<Integer> integers = new ArrayList<>();
List<Double> doubles = new ArrayList<>();

List<? extends Number> numbers1 = integers;
List<? extends Number> numbers2 = doubles;

适合"读取"数据:

java 复制代码
public static double sum(List<? extends Number> list) {
    double result = 0;

    for (Number n : list) {
        result += n.doubleValue();
    }

    return result;
}

这个方法可以接收:

java 复制代码
sum(List.of(1, 2, 3));
sum(List.of(1.5, 2.5, 3.5));

为什么可以读成 Number?

因为不管那个未知类型具体是 Integer、Double 还是 BigDecimal,它至少都是 Number。

但是,它不能安全地写入具体元素:

java 复制代码
public static void addNumber(List<? extends Number> list) {
    list.add(1);    // 错
    list.add(1.5);  // 错
}

为什么?

因为 list 可能实际是:

java 复制代码
List<Double>

如果你往里面加 Integer,就不安全。

也可能实际是:

如果你往里面加 Integer,就不安全。

也可能实际是:

java 复制代码
List<BigDecimal>

如果你往里面加 Double,也不安全。

所以对于:

java 复制代码
List<? extends Number>

编译器只知道:里面的元素"至少能当作 Number 读出来",但不知道具体能放什么进去。

唯一可以放的是:

java 复制代码
list.add(null); // 可以,但没啥实际意义

所以记住:? extends T 适合读,不适合写。

6.3 下界通配符:? super

java 复制代码
List<? super Integer>

意思是:某种未知类型的 List,但这个未知类型一定是 Integer 的父类型。

它可以接收:

java 复制代码
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

List<? super Integer> a = integers;
List<? super Integer> b = numbers;
List<? super Integer> c = objects;

这种类型适合"写入"数据:

java 复制代码
public static void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

为什么可以加 Integer?

因为不管实际列表是:

java 复制代码
List<Integer>
List<Number>
List<Object>

都能安全接收一个 Integer。

但读取时就弱了:

java 复制代码
Object x = list.get(0); // 可以
Integer y = list.get(0); // 错

为什么不能直接读成 Integer?

因为实际列表可能是:

java 复制代码
List<Object>

里面原本可能放着 "hello"、new Object() 等,并不保证取出来是 Integer。

所以记住:? super T 适合写,不适合精确读;读出来只能当作 Object。

6.5 无界通配符:?

java 复制代码
List<?>

意思是:某种未知元素类型的 List。

它和 List 不一样。

List 表示:这是一个元素类型明确为 Object 的列表,可以放任何 Object

比如:

java 复制代码
List<Object> objects = new ArrayList<>();
objects.add("hello");
objects.add(123);

而:

java 复制代码
List<?> list

表示:这是某种类型的列表,但我不知道具体是什么类型

它可能是:

java 复制代码
List<String>
List<Integer>
List<Employee>

所以不能随便加元素:

java 复制代码
list.add("hello"); // 错
list.add(123);     // 错

但可以读:

java 复制代码
Object obj = list.get(0);

因为不管里面是什么,至少都是 Object。

常见用途:

java 复制代码
public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

这个方法可以接收任何类型的 List:

java 复制代码
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));

6.6 三者对比

java 复制代码
List<? extends Number>

我不知道具体是什么列表,但元素一定是 Number 的某种子类。

可以读成 Number,基本不能写。

java 复制代码
List<? super Integer>

我不知道具体是什么列表,但它一定能接收 Integer。

可以写入 Integer,读出来只能当作 Object。

java 复制代码
List<?>

我不知道具体是什么列表。

可以读成 Object,基本不能写。

7. 反射与泛型

先给结论:

Java 泛型大多会被类型擦除,但类文件里仍然会保留一部分"泛型签名"信息,反射可以读取这些信息。

所以这里有两个层次:

  1. 运行时对象的真实类型:通常看不到具体泛型参数。

  2. 类、字段、方法声明上的泛型签名:可以通过反射读取。

  3. 运行时对象看不到泛型参数

    比如:

java 复制代码
ArrayList<String> names = new ArrayList<>();
ArrayList<Integer> numbers = new ArrayList<>();

System.out.println(names.getClass());
System.out.println(numbers.getClass());
System.out.println(names.getClass() == numbers.getClass());

输出类似:

java 复制代码
class java.util.ArrayList
class java.util.ArrayList
true

运行时没有两个类:

java 复制代码
ArrayList<String>
ArrayList<Integer>

只有一个:

java 复制代码
ArrayList

所以你不能靠对象本身判断它是 ArrayList 还是 ArrayList。

这就是类型擦除。

  1. 但声明上的泛型信息可以读取
    比如有一个类:
java 复制代码
import java.util.List;

public class UserRepository {
    private List<String> names;

    public List<String> getNames() {
        return names;
    }
}

虽然运行时 new ArrayList() 的对象不知道自己是 String 列表,但字段声明:

java 复制代码
private List<String> names;

这个"声明信息"会保留在 class 文件的 Signature 属性里。

可以用反射读取:

java 复制代码
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class ReflectionGenericDemo {
    public static void main(String[] args) throws Exception {
        Field field = UserRepository.class.getDeclaredField("names");

        Type genericType = field.getGenericType();
        System.out.println(genericType);

        if (genericType instanceof ParameterizedType pType) {
            Type rawType = pType.getRawType();
            Type[] typeArgs = pType.getActualTypeArguments();

            System.out.println(rawType);
            System.out.println(typeArgs[0]);
        }
    }
}

输出类似:

java 复制代码
java.util.List<java.lang.String>
interface java.util.List
class java.lang.String

这说明反射能看到字段声明里的 List。

  1. Class 和 Type 的区别
    反射泛型里最容易迷糊的是这两个:
java 复制代码
Class
Type

Class<?> 表示普通的运行时类:

java 复制代码
String.class
Integer.class
ArrayList.class

但泛型类型可能不只是一个普通类,比如:

java 复制代码
List<String>
T
? extends Number
List<? super Integer>

这些都不能简单用 Class 表示。

所以 Java 反射提供了更宽泛的接口:

java 复制代码
java.lang.reflect.Type

Type 有几个常见子类型:

java 复制代码
Class:普通类型,比如 String、Integer、ArrayList
ParameterizedType:参数化类型,比如 List<String>
TypeVariable:类型变量,比如 T
WildcardType:通配符类型,比如 ? extends Number
GenericArrayType:泛型数组,比如 T[]

可以这么理解:

Class 是 Type 的一种。

Type 比 Class 更能描述复杂泛型。

  1. 读取方法返回值的泛型类型
    比如:
java 复制代码
class UserService {
    public List<String> findNames() {
        return List.of("Alice", "Bob");
    }
}

反射读取:

java 复制代码
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

Method method = UserService.class.getMethod("findNames");

Type returnType = method.getGenericReturnType();

if (returnType instanceof ParameterizedType pType) {
    System.out.println(pType.getRawType()); // interface java.util.List
    System.out.println(pType.getActualTypeArguments()[0]); // class java.lang.String
}

注意:

java 复制代码
method.getReturnType()

只能得到擦除后的类型:

java 复制代码
interface java.util.List

而:

java 复制代码
method.getGenericReturnType()

可以得到:

java 复制代码
java.util.List<java.lang.String>

所以对比一下:

java 复制代码
getReturnType()           // 擦除后的 Class
getGenericReturnType()    // 带泛型信息的 Type

字段也是类似:

java 复制代码
field.getType()           // 擦除后的 Class
field.getGenericType()    // 带泛型信息的 Type

参数也是类似:

java 复制代码
method.getParameterTypes()
method.getGenericParameterTypes()
  1. 读取泛型父类
    这在框架里很常见。
    比如:
java 复制代码
class BaseDao<T> {
}

class User {
}

class UserDao extends BaseDao<User> {
}

可以通过反射读取 UserDao 继承 BaseDao 时传入的类型参数:

java 复制代码
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

Type superType = UserDao.class.getGenericSuperclass();

if (superType instanceof ParameterizedType pType) {
    Type actualType = pType.getActualTypeArguments()[0];
    System.out.println(actualType); // class User
}

很多框架会用这种方式做类型推断,比如:

java 复制代码
class UserRepository extends Repository<User, Long>

框架可以读取到:

java 复制代码
User
Long

但前提是:类型参数要写在类的继承声明上。

  1. 什么时候读不到?
    这点很重要。
    如果只是创建对象:
java 复制代码
List<String> names = new ArrayList<>();

你拿 names.getClass(),只能得到:

java 复制代码
class java.util.ArrayList

读不到 String。

因为局部变量的泛型信息通常不能通过普通反射拿到,而且对象本身也不保存 String 这个参数。

能读到的通常是:

java 复制代码
字段声明
方法返回类型声明
方法参数类型声明
父类/接口泛型声明

比如:

java 复制代码
private List<String> names;
public List<String> getNames()
class UserDao extends BaseDao<User>

这些"声明上的泛型签名"能读到。

一句话总结

反射不能从普通泛型对象里恢复被擦除的类型参数,但可以读取类文件中保留下来的字段、方法、父类、接口等声明位置的泛型签名。

所以:

java 复制代码
new ArrayList<String>().getClass()

看不到 String。

但:

java 复制代码
Field field = SomeClass.class.getDeclaredField("names");
field.getGenericType()

如果字段声明是:

java 复制代码
List<String> names;

就能看到 String。

相关推荐
Scott9999HH14 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔14 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社15 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海15 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖16 小时前
JDK 26 新特性详解
java·开发语言
马优晨17 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区19 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大19 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai19 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说20 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json