跳转到内容

反射与泛型

本文按「反射」与「泛型」两块整理:反射侧重 Class / Method / Field / Constructor 的获取与调用,泛型侧重用法、擦除与桥接方法。


反射允许在运行时检查类、接口、方法、字段、构造器等,并动态调用方法、读写字段、创建实例,而无需在编译期写死类名。典型用途:框架(Spring 依赖注入、JUnit 发现测试方法)、序列化、动态代理等。

// 1. 类名.class(编译期已知类型)
Class<?> c1 = String.class;
// 2. 对象.getClass()
String s = "hello";
Class<?> c2 = s.getClass();
// 3. Class.forName("全限定类名")(仅类名字符串已知)
Class<?> c3 = Class.forName("java.lang.String");
  • 同一类在同一个 ClassLoader 下只会有一个 Class 实例,故 c1 == c2 == c3 为 true(同一加载器时)。

1.3 反射核心 API 与“声明” vs “公有”

Section titled “1.3 反射核心 API 与“声明” vs “公有””
API 前缀范围说明
getDeclaredXxx本类声明的成员(含 private),不含继承的
getXxx本类 + 父类中的 public 成员

例如:getMethod(name, parameterTypes) 只拿 public 方法(含继承);getDeclaredMethod(...) 只拿当前类声明的方法(含 private),不包含父类。

Class<?> clazz = Class.forName("com.example.User");
// 无参构造
Constructor<?> noArg = clazz.getDeclaredConstructor();
Object obj = noArg.newInstance();
// 带参构造
Constructor<?> withArgs = clazz.getDeclaredConstructor(String.class, int.class);
Object user = withArgs.newInstance("张三", 25);
  • getDeclaredConstructor(Class<?>... parameterTypes) 可拿到任意可见性的构造器;若需调用私有构造器,需先 constructor.setAccessible(true)
  • newInstance(...) 会传播构造器内部抛出的异常(JDK 9+ 推荐用 clazz.getDeclaredConstructor().newInstance() 替代已过时的 Class.newInstance())。

获取方法

// 本类声明的指定方法(含 private)
Method m = clazz.getDeclaredMethod("methodName", String.class, int.class);
// 本类或父类 public 方法
Method mPublic = clazz.getMethod("toString");

Method.invoke 用法

// 实例方法:第一个参数为对象实例
Object result = m.invoke(instance, "arg1", 2);
// 静态方法:第一个参数传 null
Object staticResult = staticMethod.invoke(null, args);
  • 若被调方法抛出异常,会包装为 InvocationTargetException,真实原因通过 getCause() 获取。
  • 调用私有方法前需 method.setAccessible(true)(受模块系统限制时可能失败)。

Method 在 JDK 中的关键点

  • Method 继承自 Executable,内部持有方法名、参数类型、返回类型、修饰符等;实际执行通过 MethodAccessor 完成,首次调用会生成/使用 Native 或 Generated 的 accessor,后续 invoke 才真正走“调用逻辑”。
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 若为 private
// 读
Object value = field.get(instance);
// 写
field.set(instance, newValue);
  • getDeclaredField(name) 只查当前类声明字段;若取继承的 public 字段用 getField(name)
  • 静态字段:get/set 的“实例”参数可传 null

1.7 反射示例汇总(对应 PDF 中 ReflectionExample 思路)

Section titled “1.7 反射示例汇总(对应 PDF 中 ReflectionExample 思路)”
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("java.util.ArrayList");
// 构造实例
Object list = clazz.getDeclaredConstructor().newInstance();
// 调用 add(Object)
Method add = clazz.getMethod("add", Object.class);
add.invoke(list, "one");
add.invoke(list, "two");
// 调用 size()
Method size = clazz.getMethod("size");
System.out.println(size.invoke(list)); // 2
// 读内部字段(仅演示,实际 elementData 等可能不可访问)
Field sizeField = clazz.getDeclaredField("size");
sizeField.setAccessible(true);
System.out.println(sizeField.get(list)); // 2
}
}

  • 泛型类class Box<T> { private T value; ... }
  • 泛型接口interface Comparable<T> { int compareTo(T o); }
  • 泛型方法static <T> T getFirst(List<T> list) { return list.get(0); }
  • 有界类型参数<T extends Number><T extends Comparable<T>>
  • 通配符?? extends Number? super Integer
// 泛型类
public class Box<T> {
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
// 泛型方法
public static <K, V> void put(Map<K, V> map, K key, V value) {
map.put(key, value);
}
// 通配符:只读
void print(List<? extends Number> list) { ... }
// 通配符:只写(常见于 Consumer)
void addNumbers(List<? super Integer> list) { list.add(1); }

泛型只在编译期生效,编译后字节码中泛型信息被擦除,JVM 看不到 TList<String> 等,这是为兼容 1.5 之前无泛型代码的设计选择。

擦除规则简要

写法擦除后
T 无界Object
T extends NumberNumber
T extends A & B(多边界,A 为类)第一个边界类型(类)
List<String>List(原始类型)
// 源码
public class Holder<T> {
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
// 擦除后(逻辑等价,便于理解)
public class Holder {
private Object value;
public void set(Object v) { value = v; }
public Object get() { return value; }
}

因此无法在运行时用反射直接拿到“泛型参数类型”(例如无法区分 List<String>List<Integer>),只能拿到原始类型 List;若需保留类型信息,可借助「类型字面量」或显式传 Class<T>

子类继承/实现泛型父类时,擦除后父类方法签名会变成“原始类型”(如 Object),而子类重写的方法签名是具体类型(如 Integer),二者签名不一致,无法满足“重写 = 相同签名”的规则,多态会失效。编译器通过自动生成桥接方法解决:桥接方法签名与擦除后的父类一致,内部再转发到子类自己的重写方法。

示例(对应 PDF 中 BridgeMethodDemo 思路)

// 父类(擦除后 setData 参数为 Object)
public class Node<T> {
private T data;
public void setData(T data) { this.data = data; }
public T getData() { return data; }
}
// 子类(擦除后仍保留 setData(Integer))
public class IntNode extends Node<Integer> {
private Integer data;
@Override
public void setData(Integer data) { this.data = data; }
@Override
public Integer getData() { return data; }
}

擦除后:

  • NodesetData(Object)getData() -> Object
  • IntNode:若只有 setData(Integer),则与 Node.setData(Object) 签名不同,不构成重写。

编译器在 IntNode 中自动生成合成/桥接方法(源码中不可见,字节码中可见):

// 编译器生成的桥接方法(仅逻辑等价表示)
public void setData(Object data) {
setData((Integer) data); // 强转后调用本类 setData(Integer)
}
public Object getData() {
return getData(); // 拆箱/装箱后委托给本类 getData() -> Integer
}

这样,通过 Node n = new IntNode(); n.setData(x) 调用时,会走到桥接方法,再转到子类的 setData(Integer),多态得以保持。

反射时注意getDeclaredMethods() 会返回桥接方法,可通过 method.isBridge() 判断;若只关心“用户写的”方法,可过滤掉 isBridge() == true

// 泛型擦除后无法从 List 得到元素类型,只能拿到 List.class
Class<?> listClass = List.class;
// 若需在运行时使用具体类型,通常通过参数传入 Class<T>
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
String s = create(String.class);

主题要点
反射Class 三种获取方式;getDeclaredXxx 含本类私有,getXxx 含继承的 public;Constructor.newInstanceMethod.invokeField.get/set;访问私有前需 setAccessible(true)
泛型泛型类/接口/方法、有界、通配符;编译后发生类型擦除,无界 → Object,有界 → 边界类型;子类重写泛型父类方法时由编译器生成桥接方法以保持多态,反射可见 isBridge()

日期:2024-03-11。