zoukankan      html  css  js  c++  java
  • 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("
    {
    ");
                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表达式来实现类似的功能。

  • 相关阅读:
    java截取字符串
    Integer
    Sql语句常用关键字
    mybatis三种传值方式
    mybatis中的#和$的区别
    374. Guess Number Higher or Lower
    278. First Bad Version
    69. Sqrt(x)
    35. Search Insert Position
    167. Two Sum II
  • 原文地址:https://www.cnblogs.com/liyier/p/12294707.html
Copyright © 2011-2022 走看看