20. Java 使用反射(Reflection)-和内省技术

反射(Reflection)是程序的自我分析能力,通过反射可以确定类有哪些方法、有哪些构造方法以及有哪些成员变量。Java 语言提供了反射机制,通过反射机制能够动态读取一个类的信息;能够在运行时动态加载类,而不是在编译期。反射可以应用于框架开发,它能够从配置文件中读取配置信息动态加载类、创建对象,以及调用方法和成员变量。

Java 反射机制 API

Java 反射机制 API 主要是 java.lang.Class 类和 java.lang.reflect 包。

java.lang.Class 类

java.lang.Class 类是实现反射的关键所在,Class 类的一个实例表示 Java 的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和 void,void 是“无类型”,主要用于方法返回值类型声明,表示不需要返回值。Class 没有公有的构造方法,Class 实例是由 JVM 在类加载时自动创建的

方法1:调用 Object 类的 getClass()方法。
方法2:使用 Class 类的 forName()方法。
方法3:如果 T 是一个 Java 类型,那么 T.class 就代表了与该类型匹配的 Class 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main( String[] args ) throws ClassNotFoundException {
// 1.通过类型class静态变量
Class clazz;
clazz = int.class;
print(clazz);

String[] array = new String[1];
// 2.通过对象的getClass()方法
clazz = array.getClass();
print(clazz);

clazz = Class.forName("java.lang.Override");
print(clazz);
}

private static void print(Class clazz) {
System.out.println("ClassName = " + clazz.getName());
System.out.println("isAnnotation = " + clazz.isAnnotation());
System.out.println("isArray = " + clazz.isArray());
System.out.println("isPrimitive = " + clazz.isPrimitive());
System.out.println("isEnum = " + clazz.isEnum());
System.out.println("isInterface = " + clazz.isInterface());
System.out.println("---------------------");
}

每一种类型包括类和接口等,都有一个 class 静态变量可以获得 Class 实例。另外,每一个对象都有 getClass() 方法可以获得 Class 实例,该方法是由 Object 类提供的实例方法。

是否为接口: isInterface()
是否为数组对象: isArray()
是否是基本类型: isPrimitive()
获得父类: class: getSuperclass()

java.lang.reflect 包

java.lang.reflect 包提供了反射中用到类,主要的类说明如下:

  • Constructor 类:提供类的构造方法信息。
  • Field类:提供类或接口中成员变量信息。
  • Method 类:提供类或接口成员方法信息。
  • Array 类:提供了动态创建和访问 Java 数组的方法。
  • Modifier 类:提供类和成员访问修饰符信息。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
try {
// 动态加载xx类的运行时对象
Class c = Class.forName("java.lang.String");
// 获取成员方法集合
Method[] methods = c.getDeclaredMethods();
// 遍历成员方法集合
for (Method method : methods) {
// 打印权限修饰符,如public、protected、private
System.out.print(Modifier.toString(method.getModifiers()));
// 打印返回值类型名称
System.out.print(" " + method.getReturnType().getName() + " ");
// 打印方法名称
System.out.println(method.getName() + "();");
}
} catch (ClassNotFoundException e) {
System.out.println("找不到指定类");
}
}
  • 通过 Class 的静态方法 forName(String)创建某个类的运行时对象,其中的参数是类全名字符串,如果在类路径中找不到这个类则抛出 ClassNotFoundException 异常。
  • 通过 Class 的实例方法 getDeclaredMethods()返回某个类的成员方法对象数组。
  • method.getModifiers()方法返回访问权限修饰符常量代码,是int类型,例如 1 代表 public,这些数字代表的含义可以通过Modifier.toString(int)方法转换为字符串。
  • 通过 Method 的 getReturnType()方法获得方法返回值类型,然后再调用 getName()方法返回该类型的名称。
  • method.getName()返回方法名称。

创建对象

反射机制提供了另外一种创建对象方法,Class 类提供了一个实例方法 newInstance(),通过该方法可以创建对象。

下面两条语句实现了创建字符串 String 对象。

1
2
Class clz = Class.forName("java.lang.String");
String str = (String) clz.newInstance();

这两条语句相当于 String str = new String(); 语句。另外,需要注意 newInstance() 方法可以会抛出

  • InstantiationException 表示不能实例化异常
  • IllegalAccessException 是不能访问构造方法异常。

调用构造方法

调用方法 newInstance() 创建对象,这个过程中需要调用构造方法,上面的代码只是调用了 String 的默认构造方法。如果想要调用非默认构造方法,需要使用 Constructor 对象,它对应着一个构造方法,获得 Constructor 对象需要使用Class 类的如下方法:

  • Constructor[] getConstructors():返回所有公有构造方法 Constructor 对象数组。
  • Constructor[] getDeclaredConstructors():返回所有构造方法 Constructor 对象数组。
  • Constructor getConstructor(Class… parameterTypes):根据参数列表返回一个公有Constructor对象。参数 parameterTypes 是 Class数组,指定构造方法的参数列表。
  • Constructor getDeclaredConstructor(Class… parameterTypes):根据参数列表返回一个Constructor对象。参数 parameterTypes 同上。
1
2
3
4
5
6
7
8
9
10
private static void printConstructor() throws Exception {
Class clazz = String.class;
System.out.println(clazz.newInstance());

Class<?> [] parameterTypes = {String.class};
final Constructor constructor = clazz.getConstructor(parameterTypes);
Object[] initArgs = {"666"};
String str = (String) constructor.newInstance(initArgs);
System.out.println(str);
}

Java 反射机制能够在运行时动态加载类,而不是在编译期。在一些框架开发中经常将要实例化的类名保存到配置文件中,在运行时从配置文件中读取类名字符串,然后动态创建对象,建立依赖关系。采用 new 创建对象依赖关系是在编译期建立的,反射机制能够将依赖关系推迟到运行时建立,这种依赖关系动态注入进来称为依赖注入。

调用方法

通过反射机制还可以调用方法,这与调用构造方法类似。调用方法需要使用 Method 对象,它对应着一个方法,获得 Method 对象需要使用 Class 类的如下方法:

  • Method[] getMethods():返回所有公有方法 Method 对象数组。
  • Method[] getDeclaredMethods():返回所有方法 Method 对象数组。
  • Method getMethod(String name, Class… parameterTypes):通过方法名和参数类型返回公有方法 Method 对象。参数parameterTypes是Class数组,指定方法的参数列表。
  • Method getDeclaredMethod(String name, Class… parameterTypes):通过方法名和参数类型返回方法 Method 对象。参数 parameterTypes 同上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static void testMethods() throws Exception {
// 正常实现
final LocalDate now = LocalDate.now();
System.out.println(now);

final LocalDate date = LocalDate.of(2020, 1, 2);
System.out.println(date);

System.out.println(date.getYear());

// 使用 method 方式
final Class<LocalDate> clazz = LocalDate.class;
// 调用静态方法
Object obj = clazz.getMethod("now").invoke(null);
LocalDate localDate = (LocalDate) obj;
System.out.println(obj);
// 调用静态方法的有参方法
Class<?> [] parameterTypes = {int.class, int.class, int.class};
Object[] args = {2002, 1, 2};
obj = clazz.getMethod("of", parameterTypes).invoke(null, args);
System.out.println(obj);
// 调用实例的无参方法
obj = clazz.getMethod("getYear").invoke(localDate);
System.out.println(obj);
}

调用成员变量

通过反射机制还可以调用成员变量,调用方法需要使用 Field 对象,它对应着一个方法,获得 Field 对象需要使用 Class 类的如下方法:

  • Field[] getFields():返回所有公有成员变量 Field 对象数组。
  • Field[] getDeclaredFields():返回所有成员变量 Field 对象数组。
  • Field getField(String name):通过指定公共成员变量名返回 Field 对象。
  • Field getDeclaredField(String name):通过指定成员变量名返回 Field 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static void testFiled() throws Exception {
// 已知 Exception 有一个成员变量 private String detailMessage;
Throwable exception = new Throwable("hello");
System.out.println(exception.getMessage());

final Class<? extends Throwable> aClass = exception.getClass();
// 获取非静态方法的字段
final Field detailMessageField = aClass.getDeclaredField("detailMessage");
detailMessageField.setAccessible(true);
// 非静态的 get
final Object obj = detailMessageField.get(exception);
System.out.println(obj);
// 非静态的 set
detailMessageField.set(exception, "new value");
System.out.println(exception.getMessage());

// 获取静态字段的值
// private static final long serialVersionUID = -3042686055658047285L;
final Field serialVersionUIDField = aClass.getDeclaredField("serialVersionUID");
serialVersionUIDField.setAccessible(true);
System.out.println(serialVersionUIDField.get(null));
}

输出

1
2
3
4
hello
hello
new value
-3042686055658047285

设置成员变量 accessible 标志为 true,accessible 是可访问性标志,值为 true 则指示反射的对象在使用时应该取消Java语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。不仅是成员变量,方法和构造方法也可以通过 setAccessible(true) 设置,实现对私有方法和构造方法的访问

拓展

1.Type[] java.lang.Class.getGenericInterfaces()
2.Class<?>[] java.lang.Class.getInterfaces()

  • 获取类的接口实现信息
    1.返回实现接口信息的Type数组,包含泛型信息
    2.返回实现接口信息的Class数组,不包含泛型信息

细看一下,就会发现其中端倪,当你的实现接口中不包含泛型时,同样调用1方法,其返回的接口信息必然不带泛型信息的,也就是 1 中包含 2。

如何拿到接口中定义的泛型 Person?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) {
Class clz = new MyInterface<Person>() {
@Override
public Person get() {
return null;
}
}.getClass();
// 如何拿到接口中定义的泛型Person
System.out.println(((ParameterizedType) clz.getGenericInterfaces()[0]).getActualTypeArguments()[0]);

clz = new MyAbstractClass<Person>() {
@Override
public Person get() {
return null;
}
}.getClass();
// 如何拿到抽象类中定义的泛型Person
System.out.println(((ParameterizedType) clz.getGenericSuperclass()).getActualTypeArguments()[0]);


}

static interface MyInterface<T> {
T get();
}

static abstract class MyAbstractClass<T> {
abstract T get();
}

内省技术

PropertyDescriptor 类表示 JavaBean 类通过存储器导出一个属性。主要方法:

1、getPropertyType(),获得属性的 Class 对象。
2、getReadMethod(),获得用于读取属性值的方法;getWriteMethod(),获得用于写入属性值的方法。
3、hashCode(),获取对象的哈希值。
4、setReadMethod(Method readMethod),设置用于读取属性值的方法;setWriteMethod(Method writeMethod),设置用于写入属性值的方法;
将 JavaBean 中的属性封装起来进行操作。在程序把一个类当做 JavaBean 来看,就是调用 Introspector.getBeanInfo()方法,得到的 BeanInfo 对象封装了把这个类当做JavaBean看的结果信息,即属性的信息。需要导包 java.beans.*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void testPropertyDescriptor() throws Exception {
// 使用内省
final Class<App> appClass = App.class;
App app = appClass.newInstance();
PropertyDescriptor propertyDescriptor = new PropertyDescriptor("sss", appClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(app, "11111");
System.out.println(propertyDescriptor.getReadMethod().invoke(app));

BeanInfo beanInfo = Introspector.getBeanInfo(appClass);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
System.out.println(pd);
}
}

遇到过的问题

NoSuchFieldException异常原因

①没有对应字段;
②属性为私有时获取Field用的方法不是getDeclaredField。

class package.XXX cannot access a member of class YYY with modifiers “”

或者错误日志 class package.XXX cannot access a member of class YYY with modifiers “private”

在 method.invoke 方法或者 field.get 方法前加上 clazz.setAccessible(true);

Field.getGenericType()/getType()方法的功能及区别说明

Field.getGenericType()/getType()方法的功能:

  • getType():返回属性声明时类型对象(返回 class 对象)
  • getGenericType():返回属性声的 Type 类型

getType() 和 getGenericType()的不同之处:

  1. 两者返回类型不同, 一个是 Class 对象一个是 Type 接口
  2. 当属性是一个泛型, 从 getType()只能得到这个属性的接口类型
    从 getGenericType()还能得到这个泛型的参数类型。
  3. getGenericType()
    当前属性有签名属性类型就返回,否则就返回 Field.getType()

Type 是 Java 编程语言中所有类型的通用超级接口。这些类型包括原始类型、参数化类型、数组类型、类型变量和基元类型。