我第一次听说反射这个概念是在《Java编程思想》中看到的,说起这书我有些忧伤,当时自学Java,没有前辈指导,自己摸着石子过河,随便网上搜一下入门书籍,竟然清一色的推荐《Java编程思想》(当时大概2016年初,也许只是我当时知识辨别能力比较低的原因),现在看来,该书确实不适合入门,比较适合有一定开发经验的开发者。有点扯远了,拉回来,拉回来。
在《Java编程思想》中提到反射的时候,作者将其看做是Java的RTTI,RTTI即Run Time Type Infomation(运行时类型信息),但实际上RTTI可是说是特指C++的RTTI,Java是没有这个概念的,也许只是作者考虑到C++读者比较多的原因吧。
RTTI是C++语言的核心机制,它允许程序在运行时动态的决定各个对象的类型,例如经常使用到的dynamic_cast,该语法可以将某个对象在运行时动态的转换成其他任意类型,但之后是否会发生错误,就不归它管了。
反射也不是Java语言独有的概念,而是计算机科学的通用概念,在维基百科上有如下解释:
在 计算机科学 中, 反射 是指 计算机程序 在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
要注意术语“反射”和“内省“type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。
从上面描述来看,反射和RTTI确实很像,但我更倾向于将RTTI认为是反射的子集,反射包含的范围应该更广,不仅可以动态的转换类型,还可以在运行时对对象的行为(方法),状态(字段)做访问、修改等操作。
之所以要谈到RTTI,是因为如果仅仅通过《Java编程思想》了解反射,可能会对作者的意思理解不深刻,会认为反射等同于C++ 的RTTI。(我当初就是这样)
具体到Java中的反射,可以这样解释:Java反射机制让我们可以在运行时访问任何一个类的元信息,包括其接口,父类,字段,方法等。JDK还提供了API让我们可以方便使用反射机制,这些API都在java.lang.reflect包下,这些API包括Method,Field,Array等。不夸张的说,熟悉反射真的可以在Java世界里“为所欲为”,很多框架非常依赖反射技术,例如Spring,MyBaties等,Spring会在运行时获取类、方法、字段上的注解信息,然后对其做对应的处理。
类对象即Class对象,所有的类都有一个Class对象引用,该引用指向方法区中对应类的类信息,该引用在虚拟机规范中是有规定的,所以无论哪种虚拟机实现都一定会有这么一个Class对象引用,甚至基本类型都会有。例如:
public class Main { public static void main(String[] args) { //直接使用类型名.class的方式获取 Class<?> intClass = int.class; Class<?> userClass = User.class; User user = new User(); //使用对象引用.getClass()的方式获取 Class<?> userClass2 = user.getClass(); //对于基本类型,名字就是类型名,例如int类型的name就是int System.out.println(intClass.getName()); //对于类来说,名字是全限定类名 System.out.println(userClass.getName()); System.out.println(userClass2.getName()); //simpleName是将包的信息略掉,只有类名 System.out.println(userClass.getSimpleName()); } } 复制代码
上面代码用两种方式获取类对象,一种是直接使用类型名.class,一种是使用对象引用调用getClass()方法,基本类型只能使用第一种方法,引用类型两种方法都可以使用,拿到类对象的引用之后就可以“为所欲为”了!可以获取到该类有几个方法,分别是什么方法,其方法签名是怎样的等等信息,下面的代码演示了反射的简单使用:
Field类有各种对字段进行操作的API,而获取Field对象则需要先获取类对象,然后通过调用getDeclaredFields()或者getFields()来获取Field数组,其中getDeclaredFields()方法会包括私有字段,而getFields()不包括,还可以通过调用getField(String)或者getDeclaredField(String)方法来获取指定名字的字段,如果找不到就会抛出NoSuchFieldException异常。下面的代码演示了如何操作字段:
public class Main { public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException { User user = new User(); Class<?> userClass = user.getClass(); //获取字段 Field[] fields = userClass.getDeclaredFields(); for (Field field : fields) { System.out.println("field Type is " + field.getType().getName() + " ---- field name is " + field.getName()); } System.out.println("before set field value : " + user.getId()); Field field = userClass.getDeclaredField("id"); field.setAccessible(true); field.set(user, 314L); System.out.println("after set filed value : " + user.getId()); } } 复制代码
代码中先获取了字段数组,该数组包含了该类声明的所有字段,通过Field对象,我们可以获取对象名字,类型,甚至该字段在某个对象中的值。之后通过getDeclaredField("id")获取了名为id的的字段,并设置其可访问性为true,如果该字段是私有字段,不设置访问性为true的话,将无法访问该字段,紧接着使用set方法设置该字段的值,set方法有两个参数,第一个参数是要作用的对象实例,第二个参数是字段的值,下面是该程序运行的结果:
field Type is java.lang.Long ---- field name is id field Type is java.lang.String ---- field name is username field Type is java.lang.String ---- field name is password before set field value : null after set filed value : 314 复制代码
除此之外,还可以获取字段的注解、其父类等信息,Spring 框架的IOC容器有自动装配的功能,可以自动对字段进行赋值,该功能的实现原理就是依赖反射,运行时获取字段的类型信息,注解信息(用来判断是否要进行自动装配),然后在容器中查找该类的实例,查到就直接对其赋值,查不到就抛出异常。
Method类也有很多对方法进行操作的API,不过大多数都是获取方法的信息,例如方法的返回值,参数列表,参数个数,方法名等,几乎没有修改方法的API。其API的命名和Filed的极为相似,可以说是用的同一种命名模式。下面的代码演示了如何对方法进行操作:
public class Main { public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException { User user = new User(); Class<?> userClass = user.getClass(); //获取方法 Method[] methods = userClass.getDeclaredMethods(); for (Method method : methods) { System.out.println("method return type is " + method.getReturnType()); System.out.println("method name is " + method.getName()); System.out.println("method params count " + method.getParameterCount()); Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { System.out.println("param type is " + parameter.getType()); System.out.println("param name is " + parameter.getName()); } System.out.println("----------------------------------------"); } Method testMethod1 = userClass.getMethod("testMethod1", int.class, int.class); testMethod1.setAccessible(true); testMethod1.invoke(user, 1,1); //调用该方法 } } 复制代码
和Filed一样,先通过getDeclaredMethods()获取所有方法,每个方法都是一个Method对象实例,这只是JDK API对其进行的抽象,实际上在虚拟机中并没有那么简单,然后通过各种API来获取信息,在代码中获取了方法的返回值,名字,参数各种以及其参数列表,同时遍历了其参数列表。最后通过getMethod()指定相关参数获取了指定的方法对象实例,getMethod()的第一个参数是方法名,第二个参数是几个可变参数,表示参数的类对象,我定义的testMethod1只有两个int参数,所以这里传入了两个int.class对象,如果没有找到对应的方法,就抛出NoSuchMethodException异常。
随后将其设置成可访问的,并使用invoke调用该方法,invoke的第一个参数是要作用的对象实例,第二个参数也是一个可变参数,需要传入的是参数的值。最后将程序运行,大致可以看到如下输出:
.... method type is void method name is setPassword method params count 1 param type is class java.lang.String param name is arg0 ---------------------------------------- method type is void method name is testMethod1 method params count 2 param type is int param name is arg0 param type is int param name is arg1 ---------------------------------------- .... 复制代码
其实还有更多,我这里只是截取了部分。输出大部分内容符合我们预期,但参数名输出的东西是什么鬼?arg0、arg1是个什么东西?
我们一直在说反射是运行时的一种机制,即操作的对象是编译后的字节码,java8之前方法的参数名在编译之后会被类似arg0,arg1代替,java8之后提供了一个-parameters 编译选项,该选择默认是关闭的,指定之后才会打开,打开情况下,编译后的字节码就会使用源码的参数名称了。那在此之前,有什么办法运行时获取字段名称呢?答案是使用ASM等字节码技术,关于该技术的使用,本文不会涉及,有兴趣的朋友可以到网上搜索相关资料。
反射也是一项博大精深的技术,本文仅仅是简单的介绍了反射的简单使用,关于其更多的使用其实和操作字段、方法差不多,一通百通即可,实在不行再看看JDK文档就肯定会了。关于其原理,本文没涉及,原因是如果读者对虚拟机有一定了解的话,不难猜到其原理,其实这些什么字段、方法、注解、接口等信息在类加载完成之后会被存储在方法区里,同时还留了一个Class对象的引用用于访问这些信息。
很多框架都或多或少的使用到反射,实在是因为反射非常适合做这种“幕后”的事。最后,学好反射真的可以在Java世界里“为所欲为”。