java反射机制
Java反射机制
一、反射机制综述
在java中,反射是一个功能强大且复杂的机制,许多框架的底层技术和原理都与反射技术有关。因此使用反射技术的主要人员是工具构造者,而不是应用程序员。利用反射机制,我们可以用来:
1.在运行时查看对象
2.在运行时分析类的能力
3.实现通用的数组操作对象
4.利用Method对象,实现类似于C/C++中函数指针的功能
二、通过反射获取对象
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息更踪着每个对象所属的类,保存这些信息的类称为Class(就是一个类名,没有其它特殊的含义),Object类中的getClass()方法可以返回一个Class类型的实例。下面我们通过一个例子来进一步理解:
1.创建雇员类
首先定义雇员类,员工信息包括姓名、薪水和雇用日期,包含get方法和提升工资方法
public class Employee {
private String name; //姓名
private double salary; //薪水
private LocalDate hireDay; //雇用日期
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
/**
* 按百分比提升员工工资
* @param byPercent
*/
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
2.尝试获取Class对象,并对字段进行修改
public class MyTest {
public static void main(String[] args) throws Exception{
Employee e = new Employee("Harry Hacker", 560000, 2012,3,4);
System.out.println(e.getClass().getName() + " " + e.getName());
//获取Class对象的第一种方法:对象实例调用getClass()方法
Class c1 = e.getClass();
String name = c1.getName();
System.out.println(name);
//获取Class对象的第二种方法:调用静态方法forName
String className = "java.util.Random";
Class c2 = Class.forName(className);
System.out.println(c2.getName());
//获取Class对象的第三种方法:如果T是任意的Java类型,使用T.class
Class c3 = Double[].class;
System.out.println(c3.getName());
//获取雇员类的name字段,并对它进行修改
Field f = c1.getDeclaredField("name");
//由于是私有域,所以要县使用setAccessible方法来覆盖访问控制
f.setAccessible(true);
//get方法返回的是Object对象,要想正常打印,需要进行类型转换
Object v = f.get(e);
System.out.println((String) v);
//set方法可以更改对应字段的值
f.set(e, "Tom Smith");
System.out.println((String) f.get(e));
}
}
运行结果:
domain.Employee Harry Hacker
domain.Employee
java.util.Random
[Ljava.lang.Double;
Harry Hacker
Tom Smith
3.代码解读
Field类的get方法是查看对象域的关键方法,如果f为Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的值。由于Employee类中的name是一个私有域,所以如果直接调用get方法会抛出一个IllegalAccessException。因此在调用get方法之前,需要调用setAccessible方法,该方法是AccessibleObject类中的一个方法,这个类是Field、Method和Constructor类的公共父类。还有一点需要注意的是,get方法的返回值是一个Object类型。假定现在要查看salary域,它属于double类型,是一种数值类型。在Java中,数值类型不算对象。要想解决这个问题,我们可以使用Field类中的getDoule方法,返回值类型为double。实际上也可以使用get方法,此时,反射机制会自动地将这个域值打包到相应的对象包装器中,这里将会打包为Double。同理,可以用get方法获取,就可以用set方法更改。
4.应用
当获取到Class对象之后,可以调用newInstance方法调用默认的构造器(无参构造方法)新建一个对应类的实例。如果该类中没有无参构造函数,就会抛出一个异常。在Java9之后的版本中,直接用Class对象调用newInstance方法新建对象的方式已经不推荐使用。正确的做法是,先用Class对象调用getConstructor方法获取对应的构造器,然后再用构造器对象调用newInstance方法。如本案例中,可采用如下方式:
Constructor con = c1.getConstructor(String.class,double.class, int.class,int.class,int.class);
Employee e2 =(Employee)con.newInstance("123",6127,78,7,7);
这种方式有什么好处呢?在启动项目时,包含main方法的类被加载。它会加载所需要的类,这些被加载的类又会加载它们需要的类,以此类推。对一个大型应用程序来说,这会使启动应用程序消耗很多的时间,用户会因此感到不耐烦。可以使用如下技巧给用户带来一种启动速度很快的错觉:首先保证包含main方法的类没有显示调用其它类,然后一开始显示一个启动画面,通过调研Class.forName手动地加载其它的类。
三、利用反射分析类的能力
在java.lang.reflect包下,有三个非常重要的类叫做Field、Method和Constructor,非别用于描述类的域、方法和构造方法。在这三个类中,有一个叫getName的方法,可以返回对应的名称。Field类有一个getType方法,用于返回域所属类型的Class对象。Method类有一个getReturnType方法,用于返回返回值类型。Method和Constructor类有一个方法叫做getParametertypes方法,返回值是一个Object数组。此外,这三个类会员一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符的使用情况。
接下来我们通过一段代码来理解“通过反射来分析类”:
package reflection;
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest {
public static void main(String[] args) {
String name;
//从命令行或者用户输入来读取类名
if(args.length > 0)
name = args[0];
else{
Scanner in = new Scanner(System.in);
//输入的必须是完整类名
System.out.println("Please enter class name(e.g. java.util.Date)");
name = in.next();
}
try {
//如果不为空,就输出类名和它的父类
Class c1 = Class.forName(name);
Class superClass = c1.getSuperclass();
String modifiers = Modifier.toString(c1.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print("class "+name);
if (superClass != null && superClass != Object.class ) {
System.out.print("extends "+ superClass.getName());
}
System.out.print("\n{\n");
printConstructors(c1);
System.out.println();
printMethods(c1);
System.out.println();
printFields(c1);
System.out.println("}");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打印所有的构造函数
* @param c1 一个Class对象
*/
public static void printConstructors(Class c1){
Constructor[] constructors = c1.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 + "(");
//构造方法的参数类型
Class[] papramTypes = c.getParameterTypes();
for (int j = 0; j < papramTypes.length; j++) {
if (j > 0) {
System.out.print(",");
}
System.out.print(papramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* 打印所有的方法
* @param c1
*/
public static void printMethods(Class c1){
Method[] methods = c1.getDeclaredMethods();
for(Method m : methods){
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " "+ name + "(");
//打印参数类型
Class[] papramTypes = m.getParameterTypes();
for (int j = 0; j < papramTypes.length; j++) {
if (j > 0) {
System.out.print(",");
}
System.out.print(papramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* 打印类的所有域
* @param c1
*/
public static void printFields(Class c1){
Field[] fields = c1.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 + ";");
}
}
}
1.运行结果
2.代码分析
Class类中的getFieds、getMethods、 getConstrustors方法分别返回类提供的public域、方法和构造器数组,其中包括父类的公有成员。Class类的getDeclaredFields、getDeclaredMethods和getDeclaredConstrustors方法将分别返回类中声明的全部域、方法和构造器,包括私有的和受保护的成员,但不包括父类的成员。
如果我们定义一个Manager类,继承Employee类如下:
public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, int year, int month, int day){
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
运行结果为:
四、反射构造泛型数组
在Java的Arrays类中,copyOf方法可以用来拓展数组。接下来我们将自定义copyOf方法来实现拓展。一个可行的思路是,首先将所有数组转换为Object数组,然后进行拷贝,具体代码如下:
public static Object[] badCopyOf(Object[] a, int newLength){
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0 , newArray, 0, Math.min(a.length, newLength));
return newArray;
}
但是在实际使用时会产生一个问题,这段代码的返回对象是一个对象数组(Object[])类型,一个对象数组不能转换成其它类型。例如在对雇员数组(Employee[])进行拷贝时,会产生ClassCastExceptoion异常。这是因为,new分配的空间就是Object类型。将一个Employee[]临时转换成Object[]数组时,然后转换回来可以的。但是无法将一个一开始就是Object[]类型的数组转换成Employee[]数组。因此,我们需要改进一下我们的思路:
1.获取a数组的类对象
2.确认它是一个数组
3.使用Class类的getComponentType方法确定数组对应的类型。
具体代码如下:
public static Object goodCopyOf(Object a, int newLength){
Class c1 = a.getClass();
if(!c1.isArray())
return null;
Class componentType = c1.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
注意:为了实现对数值类型数组的支持,例如int[],goodCopyOf的参数应该是Object类型,而不是对象型数组Object[]。整型数组类型int[]可以转换为Object,但是不能转换为对象数组。测试代码及结果如下:
public static void main(String[] args) {
int[] a = {1,2,3};
a = (int [])goodCopyOf(a,10);
System.out.println(Arrays.toString(a));
String[] b = {"Tom","Dick","Harry"};
b = (String[]) goodCopyOf(b,10);
System.out.println(Arrays.toString(b));
System.out.println("The following call will generate an exception.");
b = (String[]) badCopyOf(b,10);
}
五、反射实现调用任意方法
在C/C++中,可以用函数指针执行任意函数。Java虽然没有提供方法指针,但是可以提供反射机制来实现类似的功能。在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名为:Object invoke(Object obj, Object... args),第一个参数是调用方法的对象,其余的提供了调用方法所需要的参数。对于静态方法,第一个参数可以被忽略,即直接设置为null。例如,在我们的案例中,可以如下使用:
Employee e = new Employee("Harry Hacker", 560000, 2012,3,4);
System.out.println(e.getClass().getName() + " " + e.getName());
//获取Class对象的第一种方法:对象实例调用getClass()方法
Class c1 = e.getClass();
String name = c1.getName();
System.out.println(name);
Method m1 = c1.getMethod("getName");
String resultName = (String) m1.invoke(e);
System.out.println(resultName);
这样就可以调用Employee类中的getName方法。
对于invoke方法的第二个参数,有两种传值方式:
Manager manager = new Manager("Bob Black", 1230, 2014,4,4);
System.out.println(manager);
Class managerClass = manager.getClass();
Constructor constructor = managerClass.getConstructor(String.class,double.class, int.class,int.class,int.class);
Manager manager1 =( Manager) constructor.newInstance("123",6127,1978,7,7);
System.out.println(manager1);
Method method = managerClass.getMethod("setBonus", double.class, boolean.class);
//invoke传参数的方法一:传入Object数组
Object[] obj = { 123, true};
method.invoke(manager, obj);
System.out.println(manager.getSalary());
//invoke传参数的方法二:直接传入值
method.invoke(manager, 63767, false);
System.out.println(manager.getSalary());
接下来的这个例子,显示了一个打印诸如Math.Sqrt、Math.Sin这样的数学函数值表的程序,这些由于都是static方法,因此第一个参数都是null。
public class MethodTableTest {
public static void main(String[] args) throws Exception{
Method square = MethodTableTest.class.getMethod("square", double.class);
Method sqrt = Math.class.getMethod("sqrt", double.class);
printTable(1,10,10,square);
printTable(1,10,10,sqrt);
}
public static double square(double x) {return x*x;}
public static void printTable(double from, double to, int n, Method f){
System.out.println(f);
double dx = (to - from)/(n-1);
for(double x = from; x <= to; x += dx){
try {
double y = (Double) f.invoke(null ,x);
System.out.printf("%10.4f | %10.4f%n" , x, y);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
上述程序表明,可以使用Method对象实现C语言中函数指针的所有操作,这种程序设计风格并不简单,出错的可能性比较大。如果在调用方法的时候提供了错误的参数,那么invoke方法就会抛出异常。另外invoke的参数和返回值必须是Object类型的,这就意味着必须进行多次的类型转换。但是这样做会使编译器错过类型检查的机会。只有等到测试阶段才会发现这些错误,这使得代码维护和修改变得更加困难。除此之外,使用反射获得方法指针的代码要比直接调用方法更慢一些。
鉴于上述原因,仅在必要的时候才使用Method对象,最好使用接口和lambda表达式来实现类似的功能。