java反射机制
一. 概述
有时候我们说某个语言具有很强的动态性,有时候我们会区分动态和静态的不同技术与作法。我们朗朗上口动态绑定(dynamic binding)、动态链接(dynamic linking)、动态加载(dynamic loading)等。然而“动态”一词其实没有绝对而普遍适用的严格定义,有时候甚至像对象导向当初被导入编程领域一样,一人一把号,各吹各的调。
一般而言,开发者社群说到动态语言,大致认同的一个定义是:“程序运行时,答应改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。
尽管在这样的定义与分类下Java不是动态语言,它却有着一个非常突出的动态相关机制:Reflection。这个字的意思是“反射、映象、倒影”,用在 Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的 class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods1。这种“看透 class”的能力(the ability of the program to examine itself)被称为introspection(内省、内观、反省)。Reflection和introspection是常被并提的两个术语。
Java如何能够做出上述的动态特性呢?这是一个深远话题,本文对此只简单介绍一些概念。整个篇幅最主要还是介绍Reflection APIs,也就是让读者知道如何探索class的结构、如何对某个“运行时才获知名称的class”生成一份实体、为其fields设值、调用其methods。本文将谈到java.lang.Class,以及java.lang.reflect中的Method、Field、ConstrUCtor等等classes。
“Class”class
众所周知Java有个Object class,是所有Java classes的继续根源,其内声明了数个应该在所有Java class中被改写的 methods:hashCode()、equals()、clone()、toString()、getClass()等。其中getClass()返回一个Class object。
Class class十分非凡。它和一般classes一样继续自Object,其实体用以表达Java程序运行时的classes和 interfaces,也用来表达enum、array、 primitive Java types(boolean, byte, char, short, int,long, float, double)以及要害词void。当一个class被加载,或当加载器(class loader)的defineClass()被JVM调用,JVM 便自动产生一个Class object。假如您想借由“修改Java标准库源码”来观察Class object的实际生成时机(例如在Class的 constructor内添加一个println()),不能够!因为Class并没有public constructor
Class是Reflection故事起源。针对任何您想探勘的class,唯有先为它产生一个Class object,接下来才能经由后者唤起为数十多个的Reflection API
二. Reflection 是Java被视为动态(或准动态)语言的一个重要特性。这个机制使应程序在运行时通过Reflection APIS取得任何一个已知名称的class的内部信息,包括其修饰符modifiers(诸如public, static 等等)、基类superclass(例如Object)、实现的interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起 methods。
Reflection 包含许多的类,例如Method类,这些类可以在java.lang.reflect包中找到。
使用Reflection 中的类需要三个步骤:
1.获取一个要操作的类的对象,该对象属于java.lang包,该对象代表一个正在运行的一个类或接口。下面的三个方法是常用的获取类对象的方法:
(1) Class c=Class.forname(“java.lang.String”);
使用.forname方法加载一个类,这里是字符串类,从而获得一个与该类对应的类对象。
(2) Class c=int.class;
(3) Class c=Integer.TYPE;
它们可获得基本类型的类信息。其中后一种方法中访问的是基本类型的封装类 (如 integer) 中预先定义好的 type 字段。
2.获取要操纵的类对象的已经声明的方法
获取类对象的方法的最简单和常用的方法是getDeclareMethods()方法。该方法返回类对象中声明过的所有方法的一个方法数组(Method[])。还有其他的方法,在后面会有所介绍。
package test1; import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { String className = "test1.Student"; Class c = Class.forName(className); //Method[] m = c.getMethods(); Method[] m =c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) { Method method = m[i]; System.out.println(method); } } catch (ClassNotFoundException e) { System.err.println(e); } } }
3.利用Reflection API操作类。
三. Java.lang.reflect包介绍
java.lang.reflect包中包含有几个常用的接口,八个类。
InvocationHandler接口:是代理实例的调用处理程序 实现的接口
Member接口:成员是一种接口,反映有关单个成员(字段或方法)或构造方法的标识信息。
AccessibleObject类:该类是域(field)对象、方法(method)对象、构造函数(constructor)对象的基础类。
Constructor类:提供一个类的构造函数的信息以及访问类的构造函数的接口。
Field类:提供一个类的域的信息以及访问类的域的接口。
Method类:提供一个类的方法的信息以及访问类的方法的接口。
Array类:该类提供动态地生成和访问JAVA数组的方法。
Modifier类:Modifier 类提供了 static 方法和常量,对类和成员访问修饰符进行解码
Proxy类:提供动态地生成代理类和类实例的静态方法。
ReflectPermission类:它允许取消由反射对象在其使用点上执行的标准 Java 语言访问检查
四. 示例与说明
3.1 查找类中声明过的相关的信息(包括字段,构造方法,方法等)
package test2; import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { String className = "test2.Student"; // 获取一个Student类的类对象cls Class cls = Class.forName(className); System.out.println("**************方法信息**************\n"); // 返回一个类声明的所有方法的方法数组 Method methlist[] = cls.getDeclaredMethods(); //Method methlist[] = cls.getMethods(); System.out.println("方法总数:" + methlist.length); for (int i = 0; i < methlist.length; i++) { Method m = methlist[i]; System.out.println("声明类: " + m.getDeclaringClass()); // 获取方法的修饰符 int modifierType = m.getModifiers(); System.out.println("修饰符:" + modifierType); System.out.println("修饰符:" + Modifier.toString(modifierType)); // 获取方法的返回类型 System.out.println("返回类型:" + m.getReturnType()); // 获取方法名称 System.out.println("方法名称: " + m.getName()); // 获取方法的参数的数组 Class pars[] = m.getParameterTypes(); for (int j = 0; j < pars.length; j++) { System.out.print("参数" + j + " :" + pars[j] + "; "); } // 获取方法抛出的异常的数组 Class exces[] = m.getExceptionTypes(); for (int j = 0; j < exces.length; j++) { System.out.print("异常" + j + ":" + exces[j] + "; "); } System.out.println("---------------------------------\n"); } } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
代码说明:
Class cls = Class.forName("method1");获取一个method1类的类对象cls。
Method methlist[] = cls.getDeclaredMethods();返回一个类声明的所有方法的方法数组。
m.getDeclaringClass();返回声明该方法的类的实例。返回值为一个class。
m.getName():返回该方法的名字,返回值为字符串类型。
Class pvec[] = m.getParameterTypes():返回该方法的参数的类型的一个数组。注意参数的返回顺序是与方法声明时的顺序是相同的。
Class evec[] = m.getExceptionTypes():获取该方法抛出的例外的一个类型数组。
m.getReturnType():返回该方法的返回值的类型。返回值是一个class。
除了上述的Method类的方法外,还有别的方法。其中比较重要的有:
Object invoke(Object obj,Object[] args)方法:对该方法进行实际的调用并执行。其中的两个参数的含义分别是调用该方法的一个类实例对象,和调用该方法的参数对象数组。具体如何应用请参看3.4节。
3.2 获取构造函数信息
Constructor ctorlist[] = cls.getDeclaredConstructors():获取该实例对象声明的所有的构造函数数组。
ct.getName():获取该构造函数的名称,返回值是一个字符串类型的变量。
ct.getDeclaringClass():返回声明该构造函数的类。返回值是一个class。
Class pvec[] = ct.getParameterTypes():返回该构造函数的参数的一个类型数组。返回的是一个class类型的数组。
Class evec[] = ct.getExceptionTypes():返回一个该构造函数的抛出的例外的一个类型数组。
除了上述的方法外,对于Constructor类还有一个很重要的方法:
Object newInstance(Object iniargs[]):实际调用该构造函数并且生成一个实例对象。
3.3 获取类中域的信息
// 获取Class 上声明的构造方法 Constructor ctorlist[] = cls.getDeclaredConstructors(); for (int i = 0; i < ctorlist.length; i++) { Constructor ct = ctorlist[i]; System.out.println("名称:" + ct.getName()); System.out.println("声明类:" + ct.getDeclaringClass()); // 获取构造分的参数 Class pvec[] = ct.getParameterTypes(); for (int j = 0; j < pvec.length; j++) { System.out.println("参数-" + j + ":" + pvec[j]); } Class evec[] = ct.getExceptionTypes(); for (int j = 0; j < evec.length; j++) { System.out.println("异常-" + j + ":" + evec[j]); } System.out.println("-----"); } // 获取指定参数的构造方法 Constructor cons = cls.getDeclaredConstructor(int.class,java.lang.String.class); // 通过构造方法创建一个类的对象 Student stu = (Student)cons.newInstance(10,"zhansan");
3.4 通过方法名调用方法
String className = "test2.Student"; // 获取一个Student类的类对象cls Class cls = Class.forName(className); Class partypes[] = new Class[2]; // 指定方法的参数是两个int类型的数据 partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; // 也可以这样指定 // partypes[0] = int.class; // partypes[1] = int.class; // 通过方法名称和参数列表获得指定的方法 Method meth = cls.getDeclaredMethod("add", partypes); //Student methobj = new Student(); // 获取指定参数的构造方法 Constructor co=cls.getDeclaredConstructor(int.class,java.lang.String.class); // 通过构造方法创建一个类的对象 Student stu = (Student)cons.newInstance(10,"zhansan"); // 创建指定的参数 Object arglist[] = new Object[2]; // 参数1指定为37 参数1指定为47 arglist[0] = new Integer(37); arglist[1] = new Integer(47); // 也可以通过如下方式指定int类型的参数 // arglist[0] = 37; // arglist[1] = 47; // 调用add方法的,stu是该类的对象, // arglist是该方法的参数,返回值是Object Object retobj = meth.invoke(stu, arglist); // 把返回值还原成Integer Integer retval = (Integer) retobj; System.out.println(retval.intValue());
方法调用的实现。
首先,类中有一个方法public int add(int a, int b)。请注意该方法的方法名’add’、两个形式参数的数据类型int以及返回值类型int。因为,这些信息对动态地调用一个类的方法是非常重要的。
接下来在主调函数中实现的功能如下:
1.Class cls = Class.forName("method2"):获取类实例对象cls。
2.Class partypes[] = new Class[2];
partypes[0] = Integer.TYPE;
partypes[1] = Integer.TYPE;
声明一个类数组,用来保存两个参数的数据类型。
3.Method meth = cls.getMethod("add", partypes);注意getMethod方法,该方法将返回一个匹配的方法。匹配的条件,有两部分来限定。一个是方法名,一个是方法的参数类型数 组。(因为JAVA中允许方法的重载,所以必须说明参数的数据类型)
参数类型数组中的各个参数类型的顺序必须与方法声明时的顺序相同。
4.method2 methobj = new method2():声明一个类method2的实例变量。
5.Object arglist[] = new Object[2];
arglist[0] = new Integer(37);
arglist[1] = new Integer(47);
声明一个对象数组,来存储两个参数实例。
6.Object retobj = meth.invoke(methobj, arglist):实际调用add函数。注意方法invoke()的两个参数,methobj是调用方法(或者是声明方法)的类的一个实 例,arglist确实被调用方法(这里是add方法)的,参数实例数组。返回值仍然是一个对象的实例retobj。
7.Integer retval = (Integer)retobj;
System.out.println(retval.intValue());将返回的对象实例,进行类型转换,并输出。
3.5 生成一个新的实例
//Student methobj = new Student(); // 获取指定参数的构造方法 Class [] consParams = new Class[2]; consParams[0] = int.class; consParams[1] = java.lang.String.class; Constructor cons = cls.getDeclaredConstructor(consParams); //Constructor cons = //cls.getDeclaredConstructor(int.class,java.lang.String.class); // 通过构造方法创建一个类的对象 //Student stu = (Student)cons.newInstance(10,"zhansan"); Object [] params = new Object[2]; params[0] = 10; params[1] = "张三"; Student stu = (Student)cons.newInstance(params); System.out.println(stu);
这个例子说明了Constructor类的newInstancce()方法的使用。其具体的过程与3.4节中使用invoke()方法类似,不再多说了。根据指定的参数类型找到相应的构造函数并执行它,以创建一个新的对象实例。使用这种方法可以在程序运行时动态地创建对象,而不是在编译的时候创建对象,这一点非常有价值。
3.6 模拟 instanceof 操作符
得到类信息之后,通常下一个步骤就是解决关于 class 对象的一些基本的问题。例如,class.isinstance 方法可以用于模拟 instanceof 操作符:
public static void main(String args[]) { try { String className = "test3.Student"; // 获取一个Student类的类对象cls Class cls = Class.forName(className); //创建了一个Student 类的 class 对象,然后检查一些对象 // 是否是 Student 的实例。 //Student 不是,但 new Student() 是。 System.out.println(cls.isInstance(new Student())); System.out.println(cls.isInstance(new String("abc"))); } catch (Exception e) { e.printStackTrace(); } }
3.7 改变字段(域)的值
reflection 的还有一个用处就是改变对象数据字段的值。reflection 可以从正在运行的程序中根据名称找到对象的字段并改变它(私有的属性不能改变)
String className = "test3.Student"; // 获取一个Student类的类对象cls Class cls = Class.forName(className); // 获取名称为name的字段 Field fld = cls.getDeclaredField("name"); Student stu = new Student(); System.out.println("name = " + stu.getName()); // 通过name字段对象,设置stu的name字段为张三 fld.set(stu, "张三"); System.out.println("name = " + stu.getName());
3.8 使用数组
reflection 的最后一种用法是创建的操作数组。数组在 java 语言中是一种特殊的类类型,一个数组的引用可以赋给 object 引用
// 取得String的Class Class cls = Class.forName("java.lang.String"); // 创建了一个10个元素的String数则 Object arr = Array.newInstance(cls, 10); // 给数组的元素赋值 Array.set(arr, 0, "Wusz"); Array.set(arr, 5, "Hello"); Array.set(arr, 6, "World"); // 取出数组里的元素 String s0 = (String) Array.get(arr, 0); String s5 = (String) Array.get(arr, 5); String s6 = (String) Array.get(arr, 6); System.out.println(s0 + " " + s5 + " " + s6);
3.9、安全性和反射
在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面接入代码,无需考虑常规的接入限制。但是,在其它情况下,不受控制的接入会带来严重的安全性风险,例如当代码在不值得信任的代码共享的环境中运行时。
3.10、反射的两个缺点
反射是一种强大的工具,但也存在一些不足。
n 性能问题。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。
n 使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂。解决这些问题的最佳方案是保守地使用反射——仅在它可以真正增加灵活性的地方——记录其在目标类中的使用。