今天还是用通俗易懂的大白话来写点我自己的理解和总结,今天要讲的是Java中比较重要的一个知识点:反射。看完如果有什么疑问的地方,可以留言讨论,也可以加我微信。我坚信,真正能让大家学到东西的文章才是好文章,这也是我最初决定写文章最主要的目标和动力,希望这篇文章对你有所帮助。ok,开始我们今天的内容。
先思考一个问题
在讲反射之前,先思考一个问题,java中如何创建一个对象,有哪几种方式?
Java中创建对象大概有这几种方式:
- 使用new关键字:这是我们最常见的也是最简单的创建对象的方式
- 使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去
- 使用反序列化:当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象
上边是Java中常见的创建对象的三种方式,其实除了上边的三种还有另外一种方式,就是接下来我们要讨论的“反射”。
反射的概述
什么是反射?按照我自己的总结和理解,反射其实就是把Java类中的各个组成部分进行解剖,并把这些解剖后得到的各个组成部分,映射成一个个的Java对象,拿到这些对象后可以做一些事情。
既然说反射是解剖Java类中的各个组成部分,所以说咱们得知道一个类中有哪些部分?一个类大致是由构造方法、方法、成员变量(字段)等信息组成的,利用反射拿到一个类的class字节码文件后,咱们可以把构造方法、方法、成员变量这些组成部分映射成一个个对象,那拿到这些映射的对象后,能干啥呢?
- 拿到映射后的构造方法,可以用它来生成对象;
- 拿到映射后的方法,可以调用它来执行对应的方法;
- 拿到映射后的字段,可以用它来获取或改变对应字段的值;
反射能干什么
说完反射的概念后,咱们说一下反射能干什么?一般来说反射是用来做框架的,或者说可以做一些抽象度比较高的底层代码,反射在日常的开发中用到的不多,但是咱们还必须搞懂它,因为搞懂了反射以后,可以帮助咱们理解框架的一些原理。所以说有一句很经典的话:反射是框架设计的灵魂。等下说完会讲一个简单的例子,来体验一下框架里大概是怎么用反射的。
怎么得到想反射的类?
刚才已经说过,反射是对一个类进行解剖,那想解剖一个东西,前提肯定是你得先得到这个东西,那么怎么得到咱们想解剖的类呢?
首先大家要明白一点,咱们写的代码是存储在后缀名是 .java的文件里的,但是它会被编译,最终真正去执行的是编译后的 .class文件。Java是面向对象的语言,一切皆对象,所以java认为这些编译后的class文件,也是一个个的对象,Java也给这事物抽象成了一种类,这个类对应的就是JDK里的Class类,大家可以去JDK的AIP文档里看一下这个类。
所以拿到这个类后,就相当于拿到了咱们想解剖的类,那怎么拿到这个类?接着看下文档,如下图
看API文档后,里边有一个 forName的方法,而且它是一个静态的方法,这样咱们直接调用它提供的这个静态方法就可以得到想反射的类了,比如用以下的方法
可以看到 Class.forName("com.cj.test.Person") 这个静态方法里接收的是一个字符串。字符串的话,我们就可以写在配置文件里,然后利用反射生成我们需要的对象,这才是我们想要的,很多框架里都有类似的配置。
解剖类
通过上边讲的方法就可以拿到想解剖的类了,接下来就可以对类进行反射解剖了。我们知道一个类里一般有构造函数、方法、成员变量(字段/属性)这三部分组成的。通过翻阅API文档,可以看到,Class这个类提供了如下常用方法:
public Constructor getConstructor(Class>…parameterTypes)public Method getMethod(String name,Class>… parameterTypes)public Field getField(String name)
public Constructor getDeclaredConstructor(Class>…parameterTypes)public Method getDeclaredMethod(String name,Class>… parameterTypes)public Field getDeclaredField(String name)
这些方法分别用于帮咱们从类中解剖出构造函数、方法和成员变量(属性)。然后把解剖出来的部分,分别映射成对应的Constructor、Method、Field对象。
- 拿到映射后的Constructor对象后,可以用它来生成对象;
- 拿到映射后的Method对象后,可以调用它来执行对应的方法;
- 拿到映射后的Field对象后,可以用它来获取或改变对应字段的值;
好,下面来分别讲一下这三部分具体的怎么操作
反射类中的构造方法
1、反射无参的构造函数
可以看到默认的无参构造方法执行了,从上边的例子看出,要想进行反射操作,首先第一步就是得到类的字节码,所以简单说一下得到类的字节码的几种方式:
- Class.forName("com.cj.test.Person"); 这就是上边我们用的方式
- 对象.getClass();
- 类名.class;
下图中对应了上边说的三种方式
2、反射“一个参数”的构造函数
3、反射“多个参数”的构造函数
4、反射“私有”的构造函数
注意:在反射私有的构造函数时,用普通的clazz.getConstructor()会报错,因为它是私有的,所以JDK提供了专门反射私有构造函数的方法 clazz.getDeclaredConstructor(int.class)和c.setAccessible(true)。
5、反射得到类中所有的构造函数
反射类中的方法
反射类中的方法,我就不再一一展开截图讲了,我直接把对应的代码全部放上来,有对应的无参的方法、一个参数的方法、多个参数的方法、私有的方法等等各种情况里边有详细的介绍,如下
package com.cj.test;
import java.util.Date;
public class Person {
public Person(){ System.out.println(\"默认的无参构造方法执行了\"); }
public Person(String name){ System.out.println(\"姓名:\"+name); }
public Person(String name,int age){ System.out.println(name+\"=\"+age); }
private Person(int age){ System.out.println(\"年龄:\"+age); }
public void m1() { System.out.println(\"m1\"); }
public void m2(String name) { System.out.println(name); }
public String m3(String name,int age) { System.out.println(name+\":\"+age); return \"aaa\"; }
private void m4(Date d) { System.out.println(d); }
public static void m5() { System.out.println(\"m5\"); }
public static void m6(String[] strs) { System.out.println(strs.length); }
public static void main(String[] args) { System.out.println(\"main\"); }
}
package com.cj.test;
import java.lang.reflect.Method;import java.util.Date;import org.junit.Test;
public class Demo2 {
@Test//public void m1() public void test1() throws Exception{ Class clazz = Class.forName(\"com.cj.test.Person\"); Person p = (Person)clazz.newInstance(); Method m = clazz.getMethod(\"m1\", null); m.invoke(p, null); }
@Test//public void m2(String name) public void test2() throws Exception{ Class clazz = Person.class; Person p = (Person) clazz.newInstance(); Method m = clazz.getMethod(\"m2\", String.class); m.invoke(p, \"张三\"); }
@Test//public String m3(String name,int age) public void test3() throws Exception{ Class clazz = Person.class; Person p = (Person) clazz.newInstance(); Method m = clazz.getMethod(\"m3\", String.class,int.class); String returnValue = (String)m.invoke(p, \"张三\",23); System.out.println(returnValue); }
@Test//private void m4(Date d) public void test4() throws Exception{ Class clazz = Person.class; Person p = (Person) clazz.newInstance(); Method m = clazz.getDeclaredMethod(\"m4\", Date.class); m.setAccessible(true); m.invoke(p,new Date()); }
@Test//public static void m5() public void test5() throws Exception{ Class clazz = Person.class; Method m = clazz.getMethod(\"m5\", null); m.invoke(null,null); }
@Test//private static void m6(String[] strs) public void test6() throws Exception{ Class clazz = Person.class; Method m = clazz.getDeclaredMethod(\"m6\",String[].class); m.setAccessible(true); m.invoke(null,(Object)new String[]{\"a\",\"b\"}); }
@Test public void test7() throws Exception{ Class clazz = Person.class; Method m = clazz.getMethod(\"main\",String[].class); m.invoke(null,new Object[]{new String[]{\"a\",\"b\"}}); }}
*****注意:可以看到上边代码里反射test6和test7方法,去执行invoke方法时,里边传的参数和其他的有点不一样。
你如果按照之前的方式用mainMethod.invoke(null,new String[]{“xxx”})这种方式去进行反射的话,执行的时候它会报参数个数不匹配的错误。这是因为jdk1.5之后出了可变参数,jdk1.4和jdk1.5处理invoke方法有区别的,分别如下:
1.4:public Object invoke(Object obj,Object[] args)1.5:public Object invoke(Object obj,Object…args)
由于JDK1.4和1.5对invoke方法的处理有区别,所以在反射类似于main(String[] args) 这种参数是数组的方法时需要特殊处理。
public static void main(String[] args),通过反射方式来调用参数是数组的这种方法时,按jdk1.5的语法,整个数组是一个参数,这是没问题的;但是按jdk1.4的语法,它会认为数组中的每个元素都是一个对应的参数。那当遇到这种当把一个数组作为参数传递给invoke方法时,java编的编译器到底会按照哪种语法进行处理呢?
jdk1.5肯定要向下兼容jdk1.4的语法,所以最后是按照jdk1.4的语法进行处理的,也就是把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”})这样来操作,因为编译器会把它用jdk1.4的语法进行解释,而不把它当作jdk1.5的语法解释,因此你用mainMethod.invoke(null,new String[]{“xxx”})这种方式调用,就会报参数个数不匹配的错误。
上述问题的解决方法:
第一种方式:mainMethod.invoke(null,(Object)new String[]{"xxx"});这种方式是你在这个数组前加个强转的操作,相当于你强制的告诉编译器你传的参数是一个整体的对象,而不是数组。所以此时就算是按照1.4的语法,编译时也不把参数当作数组看待,也就不会数组打散成若干个参数了,所以问题搞定。上边代码里的test6()这个方法,我用的就是这种方式来解决的。
第二种方式:mainMethod.invoke(null,new Object[]{new String[]{"xxx"}});这种方式,由于你传的是一个数组的参数,所以为了向下兼容1.4的语法,javac遇到数组会给你拆开成多个参数,但是由于咱们new的这个Object[ ] 数组里只有一个元素值,所以就算它拆也没关系,它拆完了得到的还是一个元素。上边代码里的test7()这个方法,我用的就是这种方式来解决的。
对上边的描述进行一下总结:在反射参数是一个数组的这种方法时,考虑到向下兼容问题,会按照JDK1.4的语法来对待,JVM会把传递的数组参数拆开,拆开就会报参数的个数不匹配的错误。解决办法:防止JVM拆开你的数组或者让它拆完得到的还是整体的一个参数。
方式一:直接强制的告诉编译器把数组看做是一个整体的Object对象,你不要给我拆开。
方式二:重新new一个Object数组,这个new的数组里的数据作为唯一的元素存在,拆完得到的还是一个整体。
反射类中的属性字段
package com.cj.test;
import java.util.Date;
public class Person {
public String name=\"李四\"; private int age = 18; public static Date time;
public int getAge() { return age; }
public Person(){ System.out.println(\"默认的无参构造方法执行了\"); }
public Person(String name){ System.out.println(\"姓名:\"+name); }
public Person(String name,int age){ System.out.println(name+\"=\"+age); }
private Person(int age){ System.out.println(\"年龄:\"+age); }
public void m1() { System.out.println(\"m1\"); }
public void m2(String name) { System.out.println(name); }
public String m3(String name,int age) { System.out.println(name+\":\"+age); return \"aaa\"; }
private void m4(Date d) { System.out.println(d); }
public static void m5() { System.out.println(\"m5\"); }
public static void m6(String[] strs) { System.out.println(strs.length); }
public static void main(String[] args) { System.out.println(\"main\"); }
}
package com.cj.test;
import java.lang.reflect.Field;import java.util.Date;import org.junit.Test;
public class Demo3 {
//public String name=\"李四\"; @Test public void test1() throws Exception{ Class clazz = Person.class; Person p = (Person)clazz.newInstance(); Field f = clazz.getField(\"name\"); String s = (String)f.get(p); System.out.println(s); //更改name的值 f.set(p, \"王六\"); System.out.println(p.name); }
@Test//private int age = 18; public void test2() throws Exception{ Class clazz = Person.class; Person p = (Person)clazz.newInstance(); Field f = clazz.getDeclaredField(\"age\"); f.setAccessible(true); int age = (Integer)f.get(p); System.out.println(age); f.set(p, 28); age = (Integer)f.get(p); System.out.println(age); }
@Test//public static Date time; public void test3() throws Exception{ Class clazz = Person.class; Field f = clazz.getField(\"time\"); f.set(null, new Date()); System.out.println(Person.time); }}
以上就是自己对Java中反射的一些总结。看完上边有关反射的东西, 对常用框架里的配置文件是不是有点思路了。
比如,上图是Spring配置文件里的常见的bean配置,现在看起来是不是可以用反射就可以简单的实现了:解析xml,然后把xml里的内容作为参数,利用反射创建对象。
拓展
1、除了上述的Spring配置文件里会用到反射生成bean对象,其他常见的MVC框架,比如Struts2、SpringMVC等等一些框架里还有很多地方都会用到反射。前端页面录入的一些信息通过表单或者其他形式传入后端,后端框架就可以利用反射生成对应的对象,并利用反射操作它的set、get方法把前端传来的信息封装到对象里。
感兴趣的话可以看下这篇:利用Java反射模拟一个Struts2框架,这篇里边包含了XML解析、反射的东西,模拟了一个Struts2的核心代码。等有时间了,我会发上来的。
2、框架的代码里经常需要利用反射来操作对象的set、get方法,来把程序的数据封装到Java对象中去。如果非常频繁的每次都使用反射来操作set、get方法进行设置值和取值的话,就太过于麻烦。所以JDK里提供了一套API,专门用于操作Java对象的属性(set/get方法),这就是内省。关于内省相关的内容我也写了一篇文章,会尽快发上来的。
3、平常用到的框架,除了配置文件的形式,现在很多都使用了注解的形式。其实注解也和反射息息相关:使用反射也能轻而易举的拿到类、字段、方法上的注解,然后编写注解解析器对这些注解进行解析,做一些相关的处理。
所以说不管是配置文件还是注解的形式,它们都和反射有关。注解和自定义注解的内容,我之前也写过:Java中的注解以及自定义注解。这一篇我也会尽快发上来的,感兴趣的小伙伴可以关注一下。