Java反射
Java的反射机制可以让我们在程序运行时动态构建一个对象,获取对象的属性、构造器以及方法。反射会破坏Java代码的封装性,所以一般只用于框架中。反射用得好,能把JVM掀个底朝天(bushi
Java代码如何被运行
源代码阶段
假若我们当前有一个普通的Java代码:
1 | public class Wizard { |
在javac之后,会生成一个Wizard.class文件,这个class文件中主要包含三个部分:成员变量,构造器和成员方法。
1 | Wizard.class |
此时我们这个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 | Wizard的Class类对象主要部分 |
因为一个类里会有多个成员变量、构造器以及方法,所以自然需要分别创建一个数组去储存他们。
运行时阶段
成功将Wizard.class加载进JVM后,JVM会创建这个对象的实例并给他分配内存空间。这时堆空间内就出现我们的Wizard对象了。
1 | +--------------+ javac +--------------+ ClassLoader +------------------------+ create object +--------------+ |
既然我们的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 | Class cls1 = Class.forName("org.example.Wizard"); |
最后我们得到的结果均为true,可见,三种方法在同一次运行过程中,得到的Class对象是同一个对象。也就是说,对于同一个*.class字节码文件,每次运行只被JVM加载一次。当然,前提是JVM用的是同一个类加载器。
Field
Java中对于Field对象的获取,提供了如下几个api:
1 | Field getField(String name) // 获取特定的public成员变量对象 |
同时,Field对象中提供了相应的get和set方法,可以获取和设置变量具体的值。以我们的Wizard对象为例:
1 | Wizard wizard = new Wizard(); |
上述代码中,我们需要new一个相应的Wizard对象,并以参数形式传入get和set才能进行变量的获取和更改。这很好理解,有了对象我才能获取和更改,没有对象我get啥set啥?可是对于一个public修饰的属性,似乎根本就不需要反射我们就能进行获取和修改,但是反射机制,能让我们同样获取到非公有属性(private, protected和default)。
1 | Wizard wizard = new 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 | Constructor<T> getConstructor(Class<?>... parameterTypes) // 获取特定的public构造器对象 |
对于获取指定的构造器,我们需要传入改构造器的参数的Class类来指定构造器。假若现在我们的Wizard拥有两个构造器:
1 | public Wizard() { |
我们可以通过getConstructor
来分别获取两个构造器:
1 | Class cls = Wizard.class; |
获取构造器的目的,显然就是为了构造一个新对象。在Constructor对象中,Java提供了一个new对象的方法:
1 | T newInstance(Object... initargs) |
实际使用:
1 | Object wizard = c1.newInstance(); |
对于无参数的构造器,Java提供了更简便的new对象的方法,不用通过Constructor,Class就可以直接创建对象:
1 | Object wizard = cls.newInstance(); |
*但此种方法在Java9之后遭到废弃,建议用Object wizard = cls.getDeclaredConstructor().newInstance()
代替。
同样,Constructor中也配备了setAccessible
来进行暴力反射的方法。
Method
同上,获取Method对象有以下四个api:
1 | Method getMethod(String name, Class<?>... parameterTypes) // 获取特定的public方法对象 |
可见,如果要获取特定方法,除了需要传入方法名称之外,还需要传入该方法各参数的Class类,因为方法有可能被重载,名称可能一样,但参数不一样。
获取Wizard类中的enchant
方法:
1 | Class cls = Wizard.class; |
显然,获取方法的目的就是执行它。Method类中提供了一个api来进行方法执行:
1 | Object invoke(Object obj, Object... args) |
调用invoke
需要传入指定方法的对象以及调用该方法所需的参数:
1 | Wizard wizard = new Wizard(); |
继承问题
假如现在我们的Wizard继承于Maiar类,Maiar类继承Ainur类,现在我们通过getFields
来获取Wizard公有成员变量,我们会发现,Maiar和Ainur的公有变量也均被获取:
1 | public java.lang.String Wizard.name |
但若通过getDeclaredFields
来获取所有成员变量,我们会发现只有Wizard的所有成员变量被获取到了,而Maiar和Ainur并没有:
1 | public java.lang.String Wizard.name |
对于获取Method,结果也同上: getMethods
会获取Wizard, Maiar, Ainur以及Object所有的public方法(因为所有类都继承于Object。同时,Object中无成员变量,所以getFields
没有获取到任何关于Object的变量):
1 | public void Wizard.enchant(java.lang.String) |
而使用getDeclaredMethods
,只能获取到Wizard的所有方法:
1 | public void Wizard.enchant(java.lang.String) |
也就是说,带有Declared的get方法只能获取到当前类的变量和方法,不管是public修饰的还是非public修饰的。而不带有Declared的普通get方法能获取到自己、父类及父类以上的所有public修饰的变量和方法。对于Constructor,不管用什么,都只能获取到当前类的构造器。