Java反射机制原理介绍和代码实例

1 篇文章 0 订阅

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.运行结果

解析Employee类的结果

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;
    }

}

运行结果为:

解析Manager类的结果

四、反射构造泛型数组

在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表达式来实现类似的功能。

  • 0
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

weixin_42364196

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值