反射
反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。
在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?
Java 反射机制主要提供了以下功能
- 在运行时(动态编译)判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法和属性。
这种动态获取信息以及动态调用对象的方法的功能称为 Java 语言的反射机制
1. Class 类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。
可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class
,这个名字很容易让人混淆。
常用方法
Object.getClass()
Object
类中的 getClass( )
方法将会返回一个 Class
类型的实例。
Employee e;
Class c1 = e.getClass();
如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。最常用的 Class 方法是 getName
, 这个方法将返回类的名字。例如,下面这条语句:
System.out.println(e.getClass().getName()); // Employee
如果类在一个包里,包的名字也作为类名的一部分:
Random generator = new Random();
Class c1 = generator.getClass();
String name = c1.getName(); // name is set to "java.util .Random"
Class.forName()
还可以调用静态方法 forName
获得类名对应的 Class 对象。
String className = "java.util.Random";
Class cl = Class.forName(className);
如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。当然, 这个方法只有在 className
是类名或接口名时才能够执行。否则,forName
方法将抛出一个 checked exception
( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器( exception handler )
在启动时, 包含 main 方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类, 以此类推。对于一个大型的应用程序来说, 这将会消耗很多时 间, 用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。 不过,要确保包含 main 方法的类没有显式地引用其他的类。
- 首先,显示一个启动画面;
- 然后,通过调用
Class.forName
手工地加载其他的类。
T.class()
获得 Class类对象的第三种方法非常简单。如果 T
是任意的 Java 类型(或 void 关键字,) T.class
将代表匹配的类对象。例如:
Class cl1 = Random.class; // if you import java.util
Class cl2 = int.class;
Class cl3 = Double[].class;
请注意,一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如, int
不是类, 但 int.class
是一个 Class 类型的对象。
虚拟机为每个类型管理一个 Class 对象。 因此,可以利用 ==
运算符实现两个类对象比较的操作。 例如,
if(e.getClass() == Employee.getClass())
还有一个很有用的方法 newInstance()
, 可以用来动态地创建一个类的实例。例如,
e.getClass().newInstance();
创建了一个与 e
具有相同类类型的实例。 newlnstance
方法调用默认的构造函数(无参构造函数)初始化新创建的对象。如果这个类没有默认的构造函数, 就会抛出一个异常
将 forName
与 newlnstance
配合起来使用, 可以根据存储在字符串中的类名创建一个对象 :
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();
2. 利用反射分析类的能力
下面简要地介绍一下反射机制最重要的内容 — 检查类的结构。
在 java.lang.reflect
包中有三个类 Field
、 Method
和 Constructor
分别用于描述类的域、 方法和构造器。
这三个类都有一个叫做 getName
的方法, 用来返回项目的名称。
Field
类有一 个 getType
方法, 用来返回描述域所属类型的 Class 对象。
Method
和 Constructor
类有能够报告参数类型的方法,Method
类还有一个可以报告返回类型的方法。
这 3 个类还有一个叫 做 getModifiers
的方法, 它将返回一个整型数值,用不同的位开关描述 public 和 static 这样 的修饰符使用状况。
另外, 还可以利用 java.lang.reflect
包中的 Modifier
类的静态方法分析 getModifiers
返回的整型数值。例如, 可以使用 Modifier 类中的 isPublic
、 isPrivate
或 isFinal
判断方法或构造器是否是 public、 private 或 final。 可以利用 Modifier.toString
方法将修饰符打印出来。
Class 类的 getFields
、 ``getMethods和
getConstructors` 方法将分别返回类提供的 public 域、 方法和构造器数组, 其中包括超类的公有成员。
Class 类的 getDeclareFields
、 getDeclareMethods
和 getDeclaredConstructors
方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。
API 如下:
3. 在运行时使用反射分析对象
从前面一节中, 已经知道如何查看任意对象的数据域名称和类型:
- 获得对应的 Class 对象。
- 通过 Class 对象调用 getDeclaredFields。
本节将进一步查看数据域的实际内容。当然, 在编写程序时, 如果知道想要査看的域名和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。
查看对象域的关键方法是 Field 类中的 get
方法。如果 f
是一个 Field
类型的对象(例如, 通过 getDeclaredFields
得到的对象) ,obj
是某个包含 f
域的类的对象,f.get(obj)
将返回一个 对象,其值为 obj 域的当前值。**这样说起来显得有点抽象,这里看一看下面这个示例的运行。
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
Field f = cl.getDeclaredField("name")
Object v = f.get(harry);
实际上,这段代码存在一个问题。由于 name
是一个私有域, 所以 get 方法将会抛出一个 IllegalAccessException
。只有利用 get 方法才能得到可访问域的值。除非拥有访问权限,否则 Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。
为了达到这个目的, 需要调用 Field、 Method 或 Constructor 对象的 setAccessible
方法。例如,
f.setAtcessible(true); // now OK to call f.get(harry);
4. 调用任意方法
在 C 和 C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针, 即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上, Java 的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为 Java 提供的 接口(interface ) (将在下一章讨论)是一种更好的解决方案。然而, 反射机制允许你调用任意方法。
在 Method 类中有一个 invoke
方法, 它允许调用包装在当前 Method 对象中 的方法。invoke
方法的签名是:
Object invoke(Object obj, Object... args)
第一个参数是隐式参数, 其余的对象提供了显式参数。
对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。 例如, 假设用 ml
代表 Employee
类的 getName
方法,下面这条语句显示了如何调用这个 方法:
String n = (String) ml.invoke(harry);
如果返回类型是基本类型, invoke
方法会返回其包装器类型。 例如, 假设 m2
表示 Employee
类的 getSalary
方法, 那么返回的对象实际上是一个 Double
, 必须相应地完成类型 转换。可以使用自动拆箱将它转换为一个 double:
double s = (Double) m2.invoke(harry);
❓ 如何得到 Method
对象呢? 当然, 可以通过调用 getDeclareMethods
方法, 然后对返回 的 Method
对象数组进行查找, 直到发现想要的方法为止。 也可以通过调用 Class 类中的 getMethod
方法得到想要的方法。然而, 有可能存在若干个相同名字的方法,因此要格外小心, 以确保能够准确地得到想要的那个方法。有鉴于此,还必须提供想要的方法的参数类型。 getMethod 的签名是:
Method getMethod(String name, Class... parameterTypes)
例如, 下面说明了如何获得 Employee
类的 getName
方法和 raiseSalary
方法的方法指针。
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary",double.class);
5. 代码实例
创建一个我们要使用反射操作的类 TargetObject
:
package com.smallbeef;
public class TargetObject {
private String value;
public TargetObject() {
value = "Java";
}
public void publicMethod(String s) {
System.out.println("I love " + s);
}
private void privateMethod() {
System.out.println("value is " + value);
}
}
使用反射操作这个类的方法以及参数
package com.smallbeef;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.print.attribute.standard.Fidelity;
public class Demo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
InstantiationException, InvocationTargetException, NoSuchFieldException {
// 获取 TargetObject 类的 Class 对象并创建 TargetObject 类实例
Class targetClass = Class.forName("com.smallbeef.TargetObject");
TargetObject targetObject = (TargetObject)targetClass.newInstance();
// 获取所有类中所有定义的方法
Method[] methods = targetClass.getDeclaredMethods();
for(Method m: methods){
System.out.println(m.getName());
}
// 调用 publicMethod 方法 (有参)
Method publicMethod = targetClass.getDeclaredMethod("publicMethod", String.class); // 获取指定方法
publicMethod.invoke(targetObject, "Computer"); // 传入参数并调用
// 获取指定参数并对参数进行修改
Field field = targetClass.getDeclaredField("value");
// 为了对类中的参数进行修改我们取消安全检查
field.setAccessible(true);
field.set(targetObject, "C++");
// 调用 private 方法 (无参)
Method privateMethod = targetClass.getDeclaredMethod("privateMethod");
// 为了调用 private 方法我们取消安全检查
privateMethod.setAccessible(true);
privateMethod.invoke(targetObject);
}
}
输出结果:
publicMethod
privateMethod
I love Computer
value is C++
6. 反射机制优缺点
优点: 动态编译。运行期类型的判断,动态加载类,提高代码灵活度。
缺点:
- 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
- 安全问题:让我们可以动态操作改变类的属性同时也增加了类的安全隐患。
7. 反射的应用场景
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:
- 我们在使用 JDBC 连接数据库时使用
Class.forName()
通过反射加载数据库的驱动程序; - Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
- 动态配置实例的属性(application.xml/yaml/properties等属性的获取)