我第一次听说反射这个概念是在《Java编程思想》中看到的,说起这书我有些忧伤,当时自学Java,没有前辈指导,自己摸着石子过河,随便网上搜一下入门书籍,竟然清一色的推荐《Java编程思想》(当时大概2016年初,也许只是我当时知识辨别能力比较低的原因),现在看来,该书确实不适合入门,比较适合有一定开发经验的开发者。有点扯远了,拉回来,拉回来。
1 RTTI和反射
在《Java编程思想》中提到反射的时候,作者将其看做是Java的RTTI,RTTI即Run Time Type Infomation(运行时类型信息),但实际上RTTI可是说是特指C++的RTTI,Java是没有这个概念的,也许只是作者考虑到C++读者比较多的原因吧。
1.1 RTTI
RTTI是C++语言的核心机制,它允许程序在运行时动态的决定各个对象的类型,例如经常使用到的dynamic_cast,该语法可以将某个对象在运行时动态的转换成其他任意类型,但之后是否会发生错误,就不归它管了。
1.2 反射
反射也不是Java语言独有的概念,而是计算机科学的通用概念,在维基百科上有如下解释:
在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
要注意术语“反射”和“内省“type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。
从上面描述来看,反射和RTTI确实很像,但我更倾向于将RTTI认为是反射的子集,反射包含的范围应该更广,不仅可以动态的转换类型,还可以在运行时对对象的行为(方法),状态(字段)做访问、修改等操作。
之所以要谈到RTTI,是因为如果仅仅通过《Java编程思想》了解反射,可能会对作者的意思理解不深刻,会认为反射等同于C++ 的RTTI。(我当初就是这样)
2 Java反射
具体到Java中的反射,可以这样解释:Java反射机制让我们可以在运行时访问任何一个类的元信息,包括其接口,父类,字段,方法等。JDK还提供了API让我们可以方便使用反射机制,这些API都在java.lang.reflect包下,这些API包括Method,Field,Array等。不夸张的说,熟悉反射真的可以在Java世界里“为所欲为”,很多框架非常依赖反射技术,例如Spring,MyBaties等,Spring会在运行时获取类、方法、字段上的注解信息,然后对其做对应的处理。
2.1 类对象
类对象即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()方法,基本类型只能使用第一种方法,引用类型两种方法都可以使用,拿到类对象的引用之后就可以“为所欲为”了!可以获取到该类有几个方法,分别是什么方法,其方法签名是怎样的等等信息,下面的代码演示了反射的简单使用:
2.2 对字段进行操作
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容器有自动装配的功能,可以自动对字段进行赋值,该功能的实现原理就是依赖反射,运行时获取字段的类型信息,注解信息(用来判断是否要进行自动装配),然后在容器中查找该类的实例,查到就直接对其赋值,查不到就抛出异常。
2.3 对方法进行操作
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等字节码技术,关于该技术的使用,本文不会涉及,有兴趣的朋友可以到网上搜索相关资料。
3 小结
反射也是一项博大精深的技术,本文仅仅是简单的介绍了反射的简单使用,关于其更多的使用其实和操作字段、方法差不多,一通百通即可,实在不行再看看JDK文档就肯定会了。关于其原理,本文没涉及,原因是如果读者对虚拟机有一定了解的话,不难猜到其原理,其实这些什么字段、方法、注解、接口等信息在类加载完成之后会被存储在方法区里,同时还留了一个Class对象的引用用于访问这些信息。
很多框架都或多或少的使用到反射,实在是因为反射非常适合做这种“幕后”的事。最后,学好反射真的可以在Java世界里“为所欲为”。