Java反射

Java的反射机制可以让我们在程序运行时动态构建一个对象,获取对象的属性、构造器以及方法。反射会破坏Java代码的封装性,所以一般只用于框架中。反射用得好,能把JVM掀个底朝天(bushi

Java代码如何被运行

源代码阶段

假若我们当前有一个普通的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Wizard {
public String name;
private double healthPoint;
protected double magicPoint;

public Wizard() {
this.name = "Gandalf";
this.healthPoint = 10;
this.magicPoint = 100;
}

public void enchant(String eleName) {
System.out.println("Generate " + eleName + " element.");
}
}

在javac之后,会生成一个Wizard.class文件,这个class文件中主要包含三个部分:成员变量,构造器和成员方法。

1
2
3
4
5
6
7
8
9
10
Wizard.class
+---------------------------------------+
| public String name; |
| private double healthPoint; |
| protected double magicPoint; |
+=======================================+
| public Wizard(){} |
+=======================================+
| public void enchant(String eleName){} |
+---------------------------------------+

此时我们这个Wizard仍处于源代码阶段,说白了,就还是在硬盘中存着。

类对象阶段

众所周知,我们这个Wizard是需要被JVM运行的,那么JVM就需要想办法把我们编译过的Wizard.class加载进去,这个过程是通过ClassLoader,类加载器,来实现的。那么加载进JVM了,现在这个Wizard.class是一个字节码文件,我们得需要一个东西来表示我们当前的Wizard.class吧。这时,JVM会创建一个Class类来对我们的字节码文件进行表示。这块所说的Class类是Java中有个类叫Class,这个Class类用来表示我们的*.class文件,就跟套娃一样。

上面说到,一个*.class字节码文件中主要是三部分:成员变量,构造器和成员方法。那么我们这个Class类,除了需要一些例如类名、编号等属性之外,最主要的肯定是要想办法存储这三个部分。Java同样提供了三个类来表示这三个部分:Field,用来储存成员变量的类;Constructor,用来储存构造器的类;Method,用来储存成员方法的类。

1
2
3
4
5
6
7
Wizard的Class类对象主要部分
+-----------------------------+
| Field[] fields; |
| Constructor[] constructors; |
| Method[] methods; |
| (...other minor attrs...) |
+-----------------------------+

因为一个类里会有多个成员变量、构造器以及方法,所以自然需要分别创建一个数组去储存他们。

运行时阶段

成功将Wizard.class加载进JVM后,JVM会创建这个对象的实例并给他分配内存空间。这时堆空间内就出现我们的Wizard对象了。

1
2
3
+--------------+ javac +--------------+ ClassLoader +------------------------+ create object +--------------+
| Wizard.java | ----> | Wizard.class | ----------> | Class object of Wizard | ------------> | new Wizard() |
+--------------+ +--------------+ +------------------------+ +--------------+

既然我们的Java代码在JVM中变成了一个Class类对象,那么我们就可以通过这个Class类对象来创建这个Wizard对象,并且可以通过其内部的Field、Constructor和Method对象数组来获取Wizard的属性、构造器以及方法。

Class类对象

获取Class类对象主要有三种方式,分别在上述三个不同的阶段来使用。

Class.forName()

这个方法用在源代码阶段,也就是说class字节码文件还没有被JVM加载进内存。

通过Class.forName("类名")来将字节码文件加载进内存,并返回Class类对象。此处的类名,包括所属的包名,例如org.example.Wizard

由于此方法需要传入一个字符串作为参数,所以通常用于配置文件。直接从文件中读取类名,然后通过字符串传递来获得到Class对象。

类名.class

此方法用在类对象阶段,也就是字节码已经加载进了内存,但还没有创建对象的时候。

此处的class是这个类的一个属性,我们通过类名.class来获取到当前类的class属性,从而获取到Class类对象。

此方法多用于调用方法时进行参数传递。例如SpringBoot在主函数中运行SpringBootApplication时,就需要传递这个类.class给run函数。

对象.getClass()

此方法用在运行时阶段,也就是内存中已经有创建完的对象了。

通过对象.getClass()方法来获取当前对象所对应的Class类对象。所有对象都会拥有getClass()方法,此方法被封装在Object对象中。

*.class加载次数

那么问题来了,上述三种方式在在同一段代码中应用的话,不同方法获取到的Class对象是否是同一个对象?我们可以用以下代码来做简单的验证:

1
2
3
4
5
6
7
Class cls1 = Class.forName("org.example.Wizard");
Class cls2 = Wizard.class;
Wizard w = new Wizard();
Class cls3 = w.getClass();

System.out.println(cls1 == cls2); //true
System.out.println(cls2 == cls3); //true

最后我们得到的结果均为true,可见,三种方法在同一次运行过程中,得到的Class对象是同一个对象。也就是说,对于同一个*.class字节码文件,每次运行只被JVM加载一次。当然,前提是JVM用的是同一个类加载器。

Field

Java中对于Field对象的获取,提供了如下几个api:

1
2
3
4
Field	getField(String name)           // 获取特定的public成员变量对象
Field[] getFields() // 获取所有public成员变量对象
Field getDeclaredField(String name) // 获取特定的成员变量对象,不受权限修饰符限制
Field[] getDeclaredFields() // 获取所有成员变量对象,不受权限修饰符限制

同时,Field对象中提供了相应的get和set方法,可以获取和设置变量具体的值。以我们的Wizard对象为例:

1
2
3
4
5
Wizard wizard = new Wizard();
Class cls = Wizard.class;
Field field = cls.getField("name");
Object value = field.get(wizard); // 获取name值
field.set(wizard, "Merlin"); //更改name值

上述代码中,我们需要new一个相应的Wizard对象,并以参数形式传入get和set才能进行变量的获取和更改。这很好理解,有了对象我才能获取和更改,没有对象我get啥set啥?可是对于一个public修饰的属性,似乎根本就不需要反射我们就能进行获取和修改,但是反射机制,能让我们同样获取到非公有属性(private, protected和default)。

1
2
3
4
Wizard wizard = new Wizard();
Class cls = Wizard.class;
Field field = cls.getDeclaredField("healthPoint");
Object value = field.get(wizard);

如果我们只是把getField换成getDeclaredField就像获取到非公有属性,在打印value的时候,会碰见如下报错:

1
Exception in thread "main" java.lang.IllegalAccessException: class Main cannot access a member of class Wizard with modifiers "private"

这时,我们需要在get之前添加一行:

1
field.setAccessible(true);

通过暴力反射,来忽略访问权限修饰符的检查,从而获取到非公有变量的值。对于get方法也同样。

Constructor

与Field类似,对于获取Constructor对象,有如下api:

1
2
3
4
Constructor<T>		getConstructor(Class<?>... parameterTypes)          // 获取特定的public构造器对象
Constructor<?>[] getConstructors() // 获取所有public构造器对象
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) // 获取特定的构造器对象,不受权限修饰符限制
Constructor<?>[] getDeclaredConstructors() // 获取所有构造器对象,不受权限修饰符限制

对于获取指定的构造器,我们需要传入改构造器的参数的Class类来指定构造器。假若现在我们的Wizard拥有两个构造器:

1
2
3
4
5
6
7
8
9
10
11
public Wizard() {
this.name = "Gandalf";
this.healthPoint = 10;
this.magicPoint = 100;
}

public Wizard(String name, double healthPoint, double magicPoint) {
this.name = name;
this.healthPoint = healthPoint;
this.magicPoint = magicPoint;
}

我们可以通过getConstructor来分别获取两个构造器:

1
2
3
Class cls = Wizard.class;
Constructor c1 = cls.getConstructor();
Constructor c2 = cls.getConstructor(String.class, double.class, double.class);

获取构造器的目的,显然就是为了构造一个新对象。在Constructor对象中,Java提供了一个new对象的方法:

1
T	newInstance(Object... initargs)

实际使用:

1
2
Object wizard = c1.newInstance();
Object anotherWizard = c2.newInstance("Merlin", 999, 999);

对于无参数的构造器,Java提供了更简便的new对象的方法,不用通过Constructor,Class就可以直接创建对象:

1
Object wizard = cls.newInstance();

*但此种方法在Java9之后遭到废弃,建议用Object wizard = cls.getDeclaredConstructor().newInstance()代替。

同样,Constructor中也配备了setAccessible来进行暴力反射的方法。

Method

同上,获取Method对象有以下四个api:

1
2
3
4
Method		getMethod(String name, Class<?>... parameterTypes)          // 获取特定的public方法对象
Method[] getMethods() // 获取所有public方法对象
Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 获取特定的方法对象,不受权限修饰符限制
Method[] getDeclaredMethods() // 获取所有方法对象,不受权限修饰符限制

可见,如果要获取特定方法,除了需要传入方法名称之外,还需要传入该方法各参数的Class类,因为方法有可能被重载,名称可能一样,但参数不一样。

获取Wizard类中的enchant方法:

1
2
Class cls = Wizard.class;
Method method = cls.getMethod("enchant", String.class);

显然,获取方法的目的就是执行它。Method类中提供了一个api来进行方法执行:

1
Object	invoke(Object obj, Object... args)

调用invoke需要传入指定方法的对象以及调用该方法所需的参数:

1
2
Wizard wizard = new Wizard();
method.invoke(wizard, "light");

继承问题

假如现在我们的Wizard继承于Maiar类,Maiar类继承Ainur类,现在我们通过getFields来获取Wizard公有成员变量,我们会发现,Maiar和Ainur的公有变量也均被获取:

1
2
3
public java.lang.String Wizard.name
public int Maiar.maiarPublicVar
public int Ainur.ainurPublicVar

但若通过getDeclaredFields来获取所有成员变量,我们会发现只有Wizard的所有成员变量被获取到了,而Maiar和Ainur并没有:

1
2
3
public java.lang.String Wizard.name
private double Wizard.healthPoint
protected double Wizard.magicPoint

对于获取Method,结果也同上: getMethods会获取Wizard, Maiar, Ainur以及Object所有的public方法(因为所有类都继承于Object。同时,Object中无成员变量,所以getFields没有获取到任何关于Object的变量):

1
2
3
4
5
6
7
8
9
10
11
12
public void Wizard.enchant(java.lang.String)
public void Maiar.maiarPublicMethod()
public void Ainur.ainurPublicMethod()
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

而使用getDeclaredMethods,只能获取到Wizard的所有方法:

1
public void Wizard.enchant(java.lang.String)

也就是说,带有Declared的get方法只能获取到当前类的变量和方法,不管是public修饰的还是非public修饰的。而不带有Declared的普通get方法能获取到自己、父类及父类以上的所有public修饰的变量和方法。对于Constructor,不管用什么,都只能获取到当前类的构造器。

Author

s.x.

Posted on

2020-07-12

Updated on

2021-11-19

Licensed under

Comments