参考自–《Java核心技术卷1》
Java 的反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中,它是 Java 组件的体系结构。
能够分析类能力的程序称为反射。反射机制的功能极其强大,它可以用来:
反射是一种功能强大且复杂的机制。
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个标识信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息找到相应的方法并执行下去。
可以通过专门的 Java 类访问这些信息(运行时类型),保存这些信息的类被称为 Class。
获取 Class 类对象的方法:
1)Object 类中的 getClass()
方法将会返回一个 Class 类型的实例。
Employee e = new Employee(); //创建一个雇员对象
Manager m = new Manager(); //创建一个经理对象
Class c1 = e.getClass();
Class c2 = m.getClass();
如同用一个 Employee 对象表示一个特定的雇员属性一样,一个 Class 对象将表示一个特定类的属性。最常用的 Class 方法是 getName
。这个方法将返回类的名字,例如:
System.out.println(e.getClass().getName()); //输出 class cn.cbq.study.sample01.Employee
System.out.println(m.getClass().getName()); //输出 class cn.cbq.study.sample01.Manager
如果类在一个包里,包的名字也作为类名的一部分输出。
2)还可以调用静态方法 forName
获取类名对应的 Class 对象:
String className = "java.util.Random";
try {
Class c = Class.forName(className);
System.out.println(c.getName()); //输出 java.util.Random
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。当然,这个方法只有在字符串为类名或接口名时才能够执行,否则,forName
方法将抛出异常(无论何时使用这个方法,都应该提供一个异常处理器)。
注:在启动 Java 程序时,包含 main 方法的类被加载,它又会加载所有它需要的类。这些被加载的类又要加载它们各自需要的类,以次类推。对于一个大型的应用程序来说,这将消耗很多时间。可以使用这样一个技巧,给人一种启动速度比较快的幻觉:在 main 方法中通过调用 Class.forName
手工地加载其他的类,但需确保包含 main 方法的类没有显式地引用其他的类。
3)如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象:
Class c1 = Random.class; //要先 import java.util.*;否则 c1 = java.util.Random.class;
Class c2 = int.class; // 输出 int
Class c3 = Double[].class; //输出 class [Ljava.lang.Double;
注意,一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类,但 int.class 是一个 Class 类型的对象。
注:Class 类实际上是一个泛型类。例如,Employee.class 的类型是 Class
。它将抽象的概念更加复杂化了,在大多数实际问题中,可以忽略类型参数,而使用原始的 Class 类。
getName 方法在应用于数组类型的时候会返回比较奇怪的名字:
虚拟机为每个类型管理一个 Class 对象。可以使用 == 运算符实现两个类对象比较的操作:
if(e.getClass()==Employee.class) ...
还有一个很有用的方法 newInstance()
,可以用来动态地创建一个类的实例:
Employee e = new Employee();
try {
Employee es = e.getClass().newInstance(); //创建了一个新的Employee实例 es
} catch (InstantiationException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
}
创建了一个与 e 具有相同类类型的实例。 newInstance()
方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。若这个类没有默认的构造器(有其他带参数的构造器),就会抛出一个异常。
将 forName
与 newInstance()
配合起来使用,就可以根据存储在字符串中的类名创建一个对象:
String className = "java.util.Random";
try {
Object obj = Class.forName(s).newInstance(); //创建一个Random类对象
Random ran = (Random)obj;
int i = ran.nextInt(); //使用 Random 对象生成一个随机 int 类型数值
} catch (InstantiationException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
注:如果需要以这种方式向希望按名称创建的类的构造器提供参数(调用带参数的构造函数),就需要使用 Constructor 类中的 newInstance()
方法。
在 java.lang.reflect 包中有三个类 Field,Method 和 Constructor 分别用于描述类的域、方法和构造器。
这三个类都有一个叫 getName
的方法,用来返回项目的名称。Field 类有一个 getType
方法,用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫 getModifies
的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外,还可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifies
返回的整型数值。例如,可以使用 Modifier 类中的 isPublic
、isPrivate
或 isFinal
判断方法或构造器是否是 public、private 或 final。另外还可以利用 Modifier.toString
方法将修饰符打印出来。
Class 类中的 getFields
、getMethods
和 getConstructors
方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclaredFields
、getDeclaredMethods
和 getDeclaredConstructors
方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
下面的案例通过输入 Java 类 分析 域,方法和构造器:
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest {
public static void main(String[] args) {
// read class name from command line args or user input
String name;
if (args.length > 0) {
name = args[0];
}else {
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (例如:java.util.Date): ");
name = in.next();
}
try {
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (supercl != null && supercl != Object.class)
System.out.print(" extends " + supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
/**
* Prints all constructors of a class
* @param cl a class
*/
public static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0)
System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0)
System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all fields of a class
* @param cl a class
*/
public static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f : fields) {
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
}
相关的API:
1)Class 类:
2)Field、Method 和 Constructor 类:
3)Modifier 类:
通过上面的内容,已经知道了如何查看任意对象的数据域名称和类型:
1.获取对应的 Class 对象
2.通过 Class 对象调用 getDeclaredFields
那么数据域的实际内容又怎么查看呢?
在编写程序时,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事。利用反射机制可以查看在编译时还不清楚的对象域。
查看对象域的关键方法是 Field 类中的 get
方法。如果 f 是一个 Field 类型的对象,obj 是某个包含 f 域的类的对象,f.get(obj)
将返回一个对象,其值为 obj 域的当前值。如下:
//Employee
public class Employee{
private String name;
private double salary;
...
}
Employee e = new Employee("zs",1000);
Class c = e.getClass();
try {
Field f = c.getDeclaredField("name"); //获取 e 对象的 name 域
Object obj = f.get(e);
System.out.println(obj);
} catch (NoSuchFieldException | IllegalAccessException ex) {
ex.printStackTrace();
}
//若 name 是公有域,则成功获取 zs
实际上,这段代码存在一个问题:由于 name 域是一个私有域,所以 get
方法将会抛出异常。只有利用get
方法才能得到可访问域的值。 除非拥有访问权限,否则 Java 安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。
反射机制默认行为受限于 Java 的访问控制。然而,如果一个 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method 或 Constructor 对象的 setAccessible
方法:
f.setAccessible(true);
f.get(e); //即使 name 域是私有域,也可以获得 name 域的值
setAccessible
方法是 AccessibleObject 类中的一个方法,它是 Field、Method 或 Constructor 类的公共超类。
get
方法还有一个需要解决的问题:name 域是一个 String ,因此把它作为 Object 返回没有什么问题;但是,若要查看 salary 域,它属于 double 类型,而 Java 的数值域不是对象,就无法直接查看(抛出异常)。
要想解决这个问题,可以使用 Field 类中的 getDouble
方法,此时,反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成 Double。
当然,可以获得就可以设置。调用 f.set(obj,value)
将 obj 对象的 f 域设置成新值 value。
f.set(e,"lisa"); //将上述的e对象的name域设置为"lisa"
java.lang.reflect 包中的 Array 类允许动态地创建数组。例如 Array 类的 copyOf
方法:
Employee[] e = new Employee[100];
...
//当数组e已经被填满后
e = Arrays.copyOf(a,2*a.length);
显然,copyOf
方法是一个通用的方法,那么它返回的数组必须为 Object 数组,那么它是如何实现的呢?
首先,我们知道,Employee[] 可以临时地转换为 Object[] 数组,然后将它转换回来也是可以的;但一个一开始就是 Object[] 的数组却无法转换成 Employee[] 数组(包括其他类型的数组)。为了编写通用的 copyOf
函数,就需要能够创建与原数组类型相同的新数组。
为此,就需要 java.lang.reflect 包中的 Array 类的一些方法。其中最关键的是 Array 类中的静态方法 newInstance
,它能构造新数组。在调用它是必须提供两个参数:原数组的元素类型,新数组的长度。
Object newArray = Array.newInstance(componentType,newLength);
可以通过 Array.getLength(obj)
获得当前数组 obj 的长度;而要获取原数组的元素类型以设置新数组的元素类型,就需要进行以下工作:
getComponentType
方法确定数组对应的类型。copyOf
方法的实现如下:
public static Object copyOf(Object obj,int newlength){
Class c1 = obj.getClass();
if(!c1.isArray())
return null;
Class componentType = c1.getComponentType();
int length = Array.getLength(obj);
Object newArray = Array.newInstance(componentType,newLength);
//public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);原数组,原数组起始位置,目标数组,目标数组开始位置,要copy的数组(原数组的)长度
System.arraycopy(obj,0,newArray,0,Math.min(length,newLength));
return newArray;
}
copyOf
方法可以用来扩展任意类型的数组,而不仅仅是对象数组:
int[] a = {1,2,3,4,5};
a = (int[])copyOf(a,10);
整型数组类型 int[] 可以被转换成 Object,但不能转换成对象数组。
反射机制允许 Java 程序员调用任意方法。
在 Method 类中有一个 invoke
方法,它允许调用包装在当前 Method 对象中的方法,invoke
方法的签名是:
Object invoke(Object obj,Object... args)
第一个参数是隐式参数,其余参数是对象提供的显式参数。
对于静态方法,第一个参数可以被省略,即可以将它设置为 null。
例如,假设 m1 代表 Employee 类的 getName
方法,下面的语句显示了如何调用这个方法:
Employee e = new Employee("zs",1000);
Method m1 = null;
Method m2 = null;
try {
m1 = Employee.class.getMethod("getName");
m2 = Employee.class.getMethod("getSalary");
String name = (String) m1.invoke(e);
double s = (Double)m2.invoke(e);
System.out.println(name);
} catch (Exception ex) {
ex.printStackTrace();
}
如果返回类型是基本类型,invoke
方法会返回其包装器类型。例如,假设有 m2 表示 Employee 的 getSalary
方法,那么实际返回的对象实际上是一个 Double ,必须相应地完成类型转换可以使用自动拆箱将它转换为一个 double.
getMethod
方法的签名是:
Method getMethod(String name,Class... parameterTypes)
parameterTypes
是传入参数的类型。
调用静态方法时:
Method m = null;
try {
m = Math.class.getMethod("sqrt",double.class);
double x = (Double)m.invoke(null,4);
System.out.println(x); //输出 2.0
} catch (Exception ex) {
ex.printStackTrace();
}
invoke
的参数和返回值都必须为 Object 类型的,这意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码错误的机会,从而只有在测试阶段才能发现这些错误。不但如此,使用反射获得方法指针的代码要比直接调用方法明显慢一些。
hod(String name,Class... parameterTypes)
parameterTypes
是传入参数的类型。
调用静态方法时:
Method m = null;
try {
m = Math.class.getMethod("sqrt",double.class);
double x = (Double)m.invoke(null,4);
System.out.println(x); //输出 2.0
} catch (Exception ex) {
ex.printStackTrace();
}
invoke
的参数和返回值都必须为 Object 类型的,这意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码错误的机会,从而只有在测试阶段才能发现这些错误。不但如此,使用反射获得方法指针的代码要比直接调用方法明显慢一些。
建议仅在有必要的时候才使用 Method 对象,最好使用接口以及 lambda
表达式。