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。

相关推荐
剑挑星河月1 小时前
35.搜索插入位置
java·数据结构·算法·leetcode
冰暮流星1 小时前
python之flask框架讲解-准备
开发语言·python·flask
海兰1 小时前
【SpringBoot 】AOP企业级权限控制方案(二)
android·java·spring boot
偏爱自由 !1 小时前
2:IDEA中git的使用--基础操作
java·git·intellij-idea
ch.ju1 小时前
Java Programming Chapter 4——Class loading
java·开发语言
LiaoWL1231 小时前
【SpringBoot合集-03】Spring Boot 启动过程学习
java·spring boot·学习
Huangjin007_1 小时前
【C++11篇(二)】右值引用、移动语义保姆级讲解!
开发语言·c++
孟浩浩3 小时前
JAVA SpringAI+阿里云百炼应用开发
java·开发语言·阿里云
钱多多_qdd3 小时前
ListUtil#split和remove搭配使用的坑
java