前言
为什么要写Java的反射?因为本人在阅读很多注入依赖这种开源库(类似Dragger2,Butterknife)的源码的时候,发现其代码都运用了大量的Java反射。本身Java反射就是框架的灵魂,为了能帮助更多的读者读懂这些开源库的代码,我决定开启一个系列文章,分别是:Java的反射;Java的注解;利用Java的反射和注解手撸一个Android注入依赖框架;ButterKnife源码解析。这篇文章是该系列的第一篇:Java的反射。
什么是Java的反射机制
java允许开发者在程序运行过程中操作(访问和修改)类的各种属性以及方法。注意加粗的几个字,“程序运行过程中”,那什么是Java文件的程序运行过程呢?
Java文件的程序运行过程
首先Java文件的程序运行过程分为三个阶段:
- Source(源代码阶段)
- Class(类对象阶段)
- Runtime(运行时)
Source(源代码阶段)
我们先创建一个Person.java文件,再用javac命令编译Person.java文件,接着就会生成一个Person.class文件,此时这两个文件都在磁盘里面,还有被加载到JVM内存里面,这个时候就处于Source(源代码阶段)
Class(类对象阶段)
当Person.class文件被类加载器(ClassLoader)加载到JVM的内存里,此时JVM运行时的方法区里面会生成一个Class类对象Class<Person> clz,这个Class类对象非常重要,这里面包含了我们对类的描述。比如说我们现在这个Person.Java文件里有成员变量、构造方法以及成员方法
public class Person extends Object {
/**
* 成员变量
* */
public String name;
int age;
protected int sex;
private int id;
/**
* 构造方法
* */
public Person(){
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 成员方法
* */
private void eat(){
System.out.println("eat-----");
}
private void drink(String drink) {
System.out.println("drink----"+drink);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
", id=" + id +
'}';
}
}
那么Class类对象是如何描述这个Person类呢?Java的思想是一切皆对象,Class类对象对类的描述也是通过对象。
- 成员变量:用的是Field对象
- 构造方法:用的是Constructor对象
- 成员方法:用的是Method对象
生成Class类对象存在JVM内存中的这个阶段就是Class类对象阶段
Runtime(运行时)
这个阶段可能大家相对就比较了解了,因为我们java程序绝大多数的情况都处于这个阶段,举个例子,当我们调用Person person = new Person();创建一个对象时,接着会在堆内存里生成一个person对象,但是这个person到底是如何被生成的呢?其实还是调用Class类对象的方法,可见Class类对象是多么重要的,而这个Class类对象也是我们实现反射的基础。
小结
这里做一个小结,因为java运行过程中这三个阶段对我们理解反射的概念非常重要,我这里画了一个图,大家理解一下
获取Class类对象
上面已经讲到了Class类对象是我们反射的基础,有了Class类对象才能实现反射,那该如何获取Class类对象呢?
java给我们提供了三种方式获取Class类对象,同时也对应上面讲的三个阶段:
- Source(源代码阶段):Class clz = Class.forName("com.example.kaka.Person");
,这个方法就是通过Java文件的全限定名(包名+类名)把它的class文件加载到JVM内存里面,此时我们就能得到Class类文件,由于是源代码阶段,所以这个方法我们经常用来加载配置文件,比如说Spring在启动的时候会加载很多的配置文件,底层实现用的就是这个方法 - Class类对象:Class clz = Person.class;这个就直接用的Person的静态属性
- Runtime(运行时):因为是运行时了,首先我们得先有Person这个对象,Pserson p = new Person;Class clz = p.getClass();
注意:通过以上三种方式生成的Class类对象都是相同的,也就是在内存的地址是一样的,这里如果你明白类的加载机制应该很容易理解,不明白的可以温习一下类的加载机制。
Class类对象能干什么
Class类对象是对类的描述,拿到了所有类的信息,理论上我们就什么都能干,比如说:获取类的构造方法、成员变量、成员方法、类名等等...下面我们分别讲一下
获取类的构造方法
对类的构造方法修饰的对象是Constructor对象,Class类对象里面提供了5个相关方法
- public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
- public Constructor<?>[] getConstructors() throws SecurityException
- public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException - public Constructor<?>[] getDeclaredConstructors() throws SecurityException
- public Constructor<?> getEnclosingConstructor()
先聊前四个方法,前两个方法的方法名没有Declared修饰,他们只能获取到用public修饰的构造方法;后面两个方法有有Declared修饰,他们可以获取所有的构造方法,不管是private还是protected修饰的,这里我们写一段代码,打印一下log
这里为了演示效果,我在Person类里多加了几个构造方法
/**
* 构造方法
*/
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Person(int id) {
this.id = id;
}
protected Person(int id, int sex) {
this.id = id;
this.sex = sex;
}
打印log的代码
Class<Person> personClass = Person.class;
System.out.println("-----------------所有public修饰的构造方法-------------------");
Constructor<?>[] constructors = personClass.getConstructors();
for (Constructor constructor1:constructors) {
System.out.println(constructor1);
}
System.out.println("---------------所有的构造方法---------------------");
Constructor<?>[] declaredConstructors = personClass.getDeclaredConstructors();
for (Constructor declaredConstructor:declaredConstructors) {
System.out.println(declaredConstructor);
}
看一下log的打印结果
我们再看有参数的那两个方法,他们是获取指定方法名的构造方法,接收的参数是参数类型的Class类对象;而无参的两个方法后面带了个s,也就是他们获取的是构造方法方法数组。
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
System.out.println("----------------指定参数类型的public修饰的构造方法-----------------");
System.out.println(constructor);
personClass.getDeclaredConstructor();
constructor = personClass.getDeclaredConstructor(int.class);
System.out.println("----------------指定参数类型的构造方法-----------------");
System.out.println(constructor);
personClass.getDeclaredConstructor();
运行结果
现在再讲一下第5个方法,getEnclosingConstructor(),官方的解释:“如果该 Class 对象表示构造方法中的一个本地或匿名类,则返回 Constructor 对象,它表示底层类的立即封闭构造方法。否则返回 null。简单来说,就是Person里面的构造方法声明了一个内部类InnerMan,此时用InnerMan的Class类对象调用getEnclosingConstructor()获取到的构造方法是Person的构造方法
public Person() {
class InnerMan {
}
InnerMan man = new InnerMan();
Constructor<?> enclosingConstructor = man.getClass().getEnclosingConstructor();
System.out.println(enclosingConstructor);
}
这个方法在实际应用中比较少见,大家知道即可
用过上面的方式我们拿到了类的构造方法对象Constructor,那我们现在要干嘛?反射机制的定义是Java允许开发者在程序运行过程中**操作(访问和修改)类的各种属性以及方法。现在拿到Constructor,我们要做的操作就是创建对象。Constructor提供一个非常重要的api
- public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
调用这个方法我们就能得到一个对象,里面接收的是一个可变参数,如果调用无参的构造方法就什么都不传,有参的就传对应的参数就行
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
Person person1 = constructor.newInstance("张三", 24);
constructor = personClass.getConstructor();
Person person2 = constructor.newInstance();
这样我们就能创建Person对象了。我们再试一下private修饰的构造方法,都知道private修饰的构造方法在类的外部是不允许被调用的,但是对反射而言不区分什么内部外部,反射都可以得到,就是这么强大
constructor = personClass.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
Person person = constructor.newInstance(1);
注意:这里最重要的一句是constructor.setAccessible(true);,当我们访问private修饰的构造方法、成员变量、成员方法,都需要调用这个API,它的意思是取消安全检查机制,这样我们就可以调用私有的构造方法了,这一点非常重要,假如我们不加这一句代码会怎么样,我们试试
直接就报错了,所以在访问私有属性以及方法之前,必须要加上obj.setAccessible(true);
获取类的成员变量
之前讲过对成员变量修饰的对象是Field,Class类对象里面提供了四个相关的方法
- public Field getField(String name) throws NoSuchFieldException
- public Field[] getFields() throws SecurityException
- public native Field getDeclaredField(String name) throws NoSuchFieldException
- public native Field[] getDeclaredFields();
跟前面的构造方法类似,首先看前两个方法的方法名没有Declared修饰,他们只能获取到用public修饰的成员变量;后面两个方法有有Declared修饰,他们可以获取所有的成员变量,不管是private还是protected修饰的;我们再看有参数的那两个方法,他们是获取指定变量名的成员变量,而无参的两个方法后面带了个s,也就是他们获取的是变量数组。
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
Person person1 = constructor.newInstance("张三", 24);
Field[] declaredFields = personClass.getDeclaredFields();//获取所有成员变量
Field id = personClass.getDeclaredField("id");//获取指定的成员变量
Field name = personClass.getField("name");//获取指定的public成员变量
现在我们拿到了成员变量了,对成员变量而言,我们的操作就是读取和修改,Field对象也提供了两个重要的api
- public native Object get(Object obj) throws IllegalArgumentException, IllegalAccessException;//获取变量的值
- public native void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException;//设置变量的值,第一个参数是对象,第二个参数是设置变量的值
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
Person person1 = constructor.newInstance("张三", 24);
// Field[] declaredFields = personClass.getDeclaredFields();//获取所有成员变量
// Field id = personClass.getDeclaredField("id");
Field name = personClass.getField("name");
Object nameVal = name.get(person1);
System.out.println(nameVal);
name.set(person1,"李四");
nameVal = name.get(person1);
System.out.println(nameVal);
这里解释一下,我们以name为例,先获取一下name的值,然后在把name改成“李四”
控制台打印的结果符合我们的预期,同样地,当我们访问被private所修饰的成员变量也要调用obj.setAccessible(true);
Field id = personClass.getDeclaredField("id");
id.setAccessible(true);
id.set(person1,3);
//这里就不做演示了
获取类的成员方法
描述方法的对象是Method,Class类对象给我们提供了5个获取Method的相关方法
- public Method[] getMethods() throws SecurityException:获取所有public修饰的方法
- public Method getMethod(String name, Class<?>... parameterTypes):获取指定方法名以及参数类型的被public修饰的成员方法
- public Method[] getDeclaredMethods() throws SecurityException:获取所有的成员方法
- public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException:获取指定方法名以及参数类型的成员方法 - public Method getEnclosingMethod():如果该 Class 对象表示成员方法中的一个本地或匿名类,则返回 Method 对象,它表示底层类的立即封闭成员方法。否则返回 null。基本上跟前面getEnclosingConstructor()是类似的。
Method[] methods = personClass.getMethods();//获取所有的public方法
Method[] declaredMethods = personClass.getDeclaredMethods();//获取所有的方法
Method drink = personClass.getDeclaredMethod("drink", String.class);//获取指定的方法名和参数类型的方法
拿到了方法对象Method以后,我们还是要操作,只需要调用方法就行了,对应的Method提供了一个重要的api
- public native Object **invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException:第一个参数传具体对象,第二个传方法里的参数,没有就不传
Method drink = personClass.getDeclaredMethod("drink", String.class);
drink.setAccessible(true);
drink.invoke(person1,"可乐");
访问私有方法同样要调用** drink.setAccessible(true);**
注意:关于Class对象还有其他一些比较常用的api,这里我们就不单独罗列出来了,直接放到下面:
- public String getName():获取类的全限定名
- public Package getPackage():获取Package对象
- public Class<?>[] getInterfaces() :获取接口数组
- public Annotation[] getAnnotations():获取注解
至此,我们就通过反射的机制拿到了一个类的所有信息,并可以操作其相关信息,建议大家自己写个小demo,跑一下
总结
反射机制可以允许我们在java程序运行过程中操作类的各种属性。java程序运行过程又分为三个阶段,Source(源代码);Class(类对象);Runtime(运行时),这三个阶段又同时提供了获取Class类对象的方法,拿到了Class对象以后我们就可以操作类的属性(成员变量、构造方法、成员方法),以此来实现了反射的效果。反射很强大,它允许我们在任何地方操作类的任何信息,因此在性能略微有影响,不过影响不大,因为硬件方面的原因,10ms和70ms对我们来说是几乎没有差距。反射也是Java框架的灵魂,掌握反射有助于我们理解使用的框架,更能帮助我们开发自己的框架。