0%

深入理解Java反射机制

前言

突然想到可以一句话概括什么是反射:如果我们不需要import就能获取一个类,那这就是用到了反射。

什么是反射

要理解什么是反射,我们先看看什么是正射,一个常见的获取Student的正射如下:

1
Student student = new Student();

通常 我们都是直接声明,或者通过new Student()直接获取一个Student类,然后再使用。而一个反射的例子如下:

1
2
3
4
5
6
/* 
* 这里的“com.demo.Student”是需要反射的类的全限定名(包名+类名)
*/
Class clz = Class.forName("com.demo.Student")

Object stu = clz.newInstance();

也就是说,先获取实例的Class类,然后再通过其Class类生成一个StudentInstance。以上两种方式(new Student和clz.newInstance)是效果是等价的,都是获取到了一个Student 的实例。
那么我们来看看Oracle官方文档中关于反射的介绍:

1
2
3
4
5
6
7
8
9
Uses of Reflection
Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.

Extensibility Features
An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
Class Browsers and Visual Development Environments
A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
Debuggers and Test Tools
Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.

大致意思是,反射使应用程序可以通过使用它们的完全限定名创建(外部的或用户定义的)对象的实例。反射通常由需要能够检查或修改在Java虚拟机中运行的应用程序的运行时行为的程序使用。这是一个相对高级的功能,应该只由对Java语言基础有很强掌握的开发人员使用。

简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

反射的作用

一个类的成员包括以下三种:域信息、构造器信息、方法信息。而反射则可以在运行时动态获取到这些信息,在使用反射时,我们常用的类有以下五种。

反射的使用

获取Class

获取Class有如下三种方式:
1、通过全限定名获取

1
Class clz = Class.forName("com.demo.Student")

2、通过类获取

1
Class clz = Student.class

3、通过实例获取

1
2
//stu是一个类的实例
Class clz = stu.getClass();

获取Constructor

一个Student类如下:

1
2
3
4
5
6
7
8
9
public class Student {
private String name;
private int age;

/*
*这里省略setter和getter
*/

}

当我们已经获取到了Class 对象clz,可以使用API public Constructor<T> getConstructor(Class<?>... parameterTypes) 获取这个Class 对象的构造器。文档对该API介绍如下:
返回一个Constructor对象,该对象反映了此Class对象表示的类的指定公共构造函数。parameterTypes参数是一个Class对象数组,这些对象按照声明的顺序标识构造函数的形参类型。 如果此Class对象表示在非静态上下文中声明的内部类,则形参类型包括显式封闭实例作为第一个参数。
所以我们可以通过如下方法获取Student的构造器

1
Constructor constructor = clz.getConstructor();

在我们没有显式指定构造函数的情况下,系统会自动生成一个空构造函数。所以这里的parameterTypes是空。接下来,我们为Student编写一个构造函数如下:

1
2
3
4
public Student(String name, int age) {
this.name = name;
this.age = age;
}

这时,我们再通过clz.getConstructor()获取构造器会发生什么?
答案是能编译,但运行报错Exception in thread "main" java.lang.NoSuchMethodException: com.demo.Student.<init>()
回顾一下,之前Java类加载中提到过的<init>是实例初始化的的一个方法。字节码文件如下:

可见,如果构造函数带参,则在<init>()方法中会有一个MethodParameters。如果无参,则没有这个东西。如果对象只含带参构造函数,那么就不能通过clz.getConstructor()获取这一带参构造函数,正如我们不能通过new Student()实例化这个对象一样。这里正确的获取构造函数的方法如下:

1
Constructor constructor = clz.getConstructor(String.class , Integer.class);

⚠️getConstructor()只能获取类中被声明为public的构造函数,而getDeclaredConstructor()则可以获取类中被声明的所有构造函数。

获取Field

getFields()方法

通过clz.getField()方法可以获取一个类中被声明为public(包括父类中)的成员变量。在这里我们对Student类进行一些修改,让它继承父类Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
/*
* 这里给sex声明为public违背了封闭原则,但只是为了演示的无奈之举
*/
public String sex ;

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public void sayHi(){
System.out.println("Hi, guys!");
}
}

然后,将Student修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student extends Person{
/*
* 这里将name的访问范围由private修改为public。
*/
public String name;
// 新增address字段,声明为protected。
protected String address;
private int age;

// 为了不占篇幅,这里忽略name和age的setter和getter

public Student(String name, int age) {
this.name = name;
this.age = age;
}

}

最后,获取Student的Field并打印。

1
2
3
4
5
6
7
Class clz = Class.forName("com.example.test.test.Student");

Field[] fields = clz.getFields();

for (Field field : fields) {
System.out.println(field);
}

结果如下:

1
2
public java.lang.String com.demo.Student.name
public java.lang.String com.demo.Person.sex

可以发现getField只获取了被声明为public的(包含父类中的)成员变量。另外,Field存储了其声明类型访问控制修饰符字段命名。如果我们只需要获取其名字,可以通过field.getName()的方式,输出如下:

1
2
name
sex

getField(String name)方法

通过public Field getField(String name)方法,我们可以获取指定的成员变量,这里的参数String name是成员变量的名字。比如我们要获取Student的sex字段,可以通过如下方式:

1
2
Field field = clz.getField("sex");
System.out.println(field);

输出结果如下:

1
public java.lang.String com.example.test.test.Person.sex

getDeclaredFields()方法

顾名思义,getDeclaredFields()方法可以获取在一个类中(不包含父类)声明的所有成员变量。我们获取并打印Student的DeclaredFields如下:

1
2
3
4
5
Field[] fields = clz.getDeclaredFields();

for (Field field : fields) {
System.out.println(field);
}

输出结果如下:

1
2
3
public java.lang.String com.example.test.test.Student.name
protected java.lang.String com.example.test.test.Student.address
private int com.example.test.test.Student.age

getDeclaredField(String name)方法

正如getField()方法,通过public Field getDeclaredField(String name)方法,我们可以获取在本类中声明的指定的成员变量。如下:

1
2
Field field = clz.getDeclaredField("age");
System.out.println(field);

输出结果如下:

1
private int com.example.test.test.Student.age

文档

最后,我们回过头来看一下Field的文档。

1
2
A Field provides information about, and dynamic access to, a single field of a class or an interface. The reflected field may be a class (static) field or an instance field.
A Field permits widening conversions to occur during a get or set access operation, but throws an IllegalArgumentException if a narrowing conversion would occur.

意思是Field提供有关类或接口的单个字段的信息和动态访问。 反射字段可以是类(静态)字段或实例字段。
Field允许在 get 或 set 访问操作期间发生扩大转换,但如果发生缩小转换,则会引发IllegalArgumentException。
⚠️可能有人对这里的扩大转换缩小转换有点不太理解。下面举个例子:
我们给Student声明一个新的成员变量id,初值为8;

1
2
3
4
public Class Student{
//其他省略
public int id = 8
}

然后通过getField()方法获取这一字段,

1
Field id = clz.getField("id");

接着,我们实例化一个Student对象。

1
Object student  = clz.newInstance();

然后,获取student的id变量的值,用int正常接收;

1
2
3
4
5
6
/*
* 这里通过变量的get方法获取对象中该变量的值是不是有点奇怪,这里后面再解释。
*/
int id_v = id.getInt(student);
//打印该值
System.out.print(id_v);

结果如下:

1
8

接着,我们看下什么是扩大转换。我们用一个Double类型的对象来接收这一int:

1
2
Double id_v = id.getDouble(student);
System.out.print(id_v);

结果是:

1
8.0

最后,我们看下什么是缩小转换。我们用一个Short类型的对象来接收这一int:

1
2
short id_v = id.getShort(student);
System.out.print(id_v);

结果是:

1
2
3
4
Exception in thread "main" java.lang.IllegalArgumentException: Attempt to get int field "com.example.test.test.Student.age" with illegal data type conversion to short
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.newGetIllegalArgumentException(UnsafeFieldAccessorImpl.java:69)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.newGetShortIllegalArgumentException(UnsafeFieldAccessorImpl.java:128)
at java.base/jdk.internal.reflect.UnsafeIntegerFieldAccessorImpl.getShort(UnsafeIntegerFieldAccessorImpl.java:52)

也就是说无法将int转化为short。
总结如下:
扩大转换就是将short转换为int、int转换为long、char转换为String等。缩小类型转换就是将int转换为short,long转换为int等

获取Method

getMethods()、getDeclaredMethods()

getMethods()、getDeclaredMethods()方法和getFields()、getDeclaredFields()一样,就不叙述过多,这里主要讲一下特别的点,
由于Java中一切皆Object的思想,所以所有的Class除了自己声明的方法,还有继承自Object的如下方法

1
2
3
4
5
6
7
8
9
1public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
2public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
3public final void java.lang.Object.wait() throws java.lang.InterruptedException
4public boolean java.lang.Object.equals(java.lang.Object)
5public java.lang.String java.lang.Object.toString()
6public native int java.lang.Object.hashCode()
7public final native java.lang.Class java.lang.Object.getClass()
8public final native void java.lang.Object.notify()
9public final native void java.lang.Object.notifyAll()

getMethod(String name, Class... parameterTypes)、getDeclaredMethod(String name, Class… parameterTypes)

和getField(String name)不同的是,它多了一个参数数组parameterTypes,它们是该Method的参数数组。
除此之外,就没啥好说的了。

实例调用方法(invoke函数介绍)

需要注意的是,正如Field,我们通过反射获取到的Method也只是一个Method,它只是一个method的影子、模版。如果我们要调用这个方法,需要指定是哪个类调用了这个方法。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取Class对象
Class clz = Class.forName("com.example.test.test.Student");
//获取setAge方法
Method method = clz.getMethod("setAge",int.class);
/*
* 获取Student实例,
* 这里是给Student写了一个空构造函数后才能这么写,否则会报错
*/
Object student = clz.newInstance();
// student调用setAge方法,设置age为18
method.invoke(student,18);
// 获取getAge方法
Method getAge = clz.getMethod("getAge");
// 打印student调用getAge返回的值
System.out.println(getAge.invoke(student));

运行结果如下:

1
18

获取实例(Object)

通过Class直接生成

Class实例拥有一个newInstance()方法,该方法可以快速生成一个实例。如下:

1
Object student = clz.newInstance();

可是当我们运行时,却得到如下报错:

1
2
3
4
Exception in thread "main" java.lang.InstantiationException: com.example.test.test.Student
at java.base/java.lang.Class.newInstance(Class.java:571)
at com.example.test.test.Student.main(Student.java:49)
Caused by: java.lang.NoSuchMethodException: com.demo.Student.<init>()

我们查看newInstance()的文档如下:

1
Creates a new instance of the class represented by this Class object. The class is instantiated as if by a new expression with an empty argument list. The class is initialized if it has not already been initialized.

创建由此Class对象表示的类的新实例。 类被实例化,就好像由一个带有空参数列表的new表达式一样。回想一下,我们的Student类好像只有一个带参构造函数,没有空构造函数,所以才会导致报错。在给Student添加一个无参构造函数后,不再报错,顺利获得一个Student实例。
⚠️newInstance()被标记 @Deprecated(since=”9”)。意思是这个方法在jdk9以后被弃用了。可以替换为clz.getDeclaredConstructor().newInstance()。
这个替代方案的实质还是使用Constructor生成实例,所以调用一个带参构造函数Student(String name, int age)生成实例如下:

1
Object student = clz.getDeclaredConstructor(String.class,int.class).newInstance("ceaser",11);

通过Constructor生成

通过上面的介绍知道,jdk将clz的newInstance废弃的替代方案为Constructor。

1
Object student = clz.getDeclaredConstructor(String.class,int.class).newInstance("ceaser",11);

其实等价于

1
2
Constructor constructor = clz.getDeclaredConstructor(String.class,int.class);
Object student = constructor.newInstance("ceaser",11);
-------------------本文结束 感谢阅读-------------------