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表达式来实现类似的功能。
来源:https://www.cnblogs.com/liyier/p/12294707.html