06. 反射核心知识点汇总

给初学者的建议

  • 很多初学者视反射为洪水猛兽,觉得这东西非常难,对其产生惧怕心理,进而导致自己写的少,用的少,不多思考,我觉得作为一个程序员在不是特别清楚某个技术点的时候,最好对其保持一种谦恭的态度,不要稍微了解一点皮毛就得意洋洋说这个技术点也就那样,没啥大不了的.但是谦恭绝不等于惧怕,对其不是很了解,那么就接着深入的了解它,探索它的核心.始终对自己不了解,不熟悉,感兴趣的技术抱有一颗好奇心.看的多了,用的多了,自然而然用起来也会更加的得心应手了.

反射的理解★★★

  • 在我刚开始接触反射的时候,一直有一个问题想不通:JAVA中规定了各种权限修饰符,给了各种限定,换句话说就是给了一套规则,那么在JAVA程序中不是所有的技术都应该遵循这套规则吗?JAVA提供了反射,在通过它获取数据的时候可以完全无视Java规则来获取任意字段值,调用任意方法,这完全是Java中的外挂啊,黑客利用反射岂不是可以为所欲为了??反射到底在Java中是出于什么目的而存在的?

    • 首先应该知道的是web应用是在公司内的服务器内运行的,要想用反射进行操作,必须要在该web应用内代码里面进行操作,黑客是不可能根据服务器地址远程通过反射对你的web应用内的代码进行操作的,换句话说,反射是提供给开发人员使用的,平常开发人员进行项目开发的时候需要遵守Java规范,但是若是涉及到的需求需要对多个类中的多个方法进行统一的操作,或者在某些状况下不得不获取某个类的私有字段,若是还在Java规范内是肯定做不到的.目前流行的诸如Spring,SpringMVC,Struts2这些都是大量用到了反射技术才做出来的.有句话说的好,反射是框架的灵魂 ,举个例子,行军打仗的时候, 会有很多的士兵,这些士兵都有吃饭,训练,沙场杀敌的权利,但是若是让一个士兵去号令全军是做不到的,因为他们没有这个权利.然后行军打仗的时候肯定会派一个指挥者,将军,将军想要号令全军,必须要出示虎符证明自己的身份.然后才有号令全军的权利,若是在平常每个士兵都做自己日常的事情,比如训练,吃饭等等.但是若是上战场杀敌若是没有将军指挥那么就算有再多的士兵也只是一盘散沙.类比于Java,一些需求类都相当于士兵,反射相当于虎符,类中带有反射的类相当于将军,将军通过虎符来统一调配控制士兵相当于框架利用反射去处理类.

反射的使用

反射之字节码对象★★★

  • 想要利用反射,必须要先有字节码对象

    字节码对象获得三种方式:
    Class<?> clazz = Class.forName("com.itheima.Student");
    Class<?> clazz  = Student.class;
    Class<?> clazz = new Student().getClass();
  • 注意: 类的字节码对象不同于根据类创建出的实例对象,Student.class此文件加载到JVM虚拟机内存后会产生一个对应的字节码文件对象,而且在整个JVM内存中只能存在一个与Student类对应的字节码文件对象.该对象存储在内存中的方法去,通过该对象采用反射的相关方法来获取或调用相关方法及属性.根据一个类,可以通过new的方式创建很多个该类的实例对象.实例对象存在于堆内存中.实例对象可通过对象"点"的方式直接调用该类中的任意方法.

    graph LR;
    A[Student.class文件] -->|加载到jvm内存中| B(Student.class对象);
    B--> |Student s1 = new Student| C[s1实例对象];

反射之构造器对象★★★

  • 根据类的字节码文件对象可以获取其构造器对象,看好了,这里说的是对象 ,这个对象中包含了与对应构造函数的相关属性,比如,构造函数名称,构造函数的参数类型等等。

构造器对象的获取

通过类的字节码文件对象获取构造函数的方式有如下几种:

第一种:获取该类中所有公有构造函数
Constructor<?>[] constructors = clazz.getConstructors();
第二种:获取该类中所有构造函数,包括公有和私有
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
第三种:获取指定的构造函数对象
Constructor<?> constructor1 = clazz.getConstructor(Class<?>... parameterTypes);
  • 针对于第一种与第二种方式,返回值类型均为构造器对象数组,若要获取具体的构造器对象,则需要对数组进行遍历才行。

  • 针对于第三种方式,首先注意getConstructor()方法,里面需要传递的是一个参数类型的字节码文件对象的可变数组,举个例子,假如说的一个类的构造函数为public Student(String username,int age){} 那么若是要通过反射获取该构造函数,对应着就要写Constructor<?> constructor = clazz.getConstrutor(String.class,int.class) 也就是说,类的构造函数中要传递什么样的参数,比如StringintInteger,HttpServletRequest,那么在采用反射获取的时候只需要对应着写String.classint.class,Integer.class,HttpServletRequest.class,即可,注意,写的先后顺序必须与构造函数中的先后顺序对应且一致。

构造器对象的调用

package com.itheima.bean;

/**
 * 学生pojo类
 *
 * @author tengchao
 * @create 2017-12-08-18:11.
 */
public class Student {
    public Student(){
        System.out.println("无参构造");
    }

    public Student(String username, Integer age) {
        this.username = username;
        this.age = age;
        System.out.println("有参构造,用户名:"+username+",年龄:"+age);
    }
    private String username;
    private Integer age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

假如说存在上述的一个类,我们都知道,当通过Student student = new Student(); 去创建一个实例对象时,就会通过调用该类的无参构造函数去初始化该实例,当我们通过Studeng student = new Student("zs",18); 去创建一个实例对象时,就会通过调用该类的有参构造函数去初始化该实例。那么,当我们通过反射获取对应的构造器对象后,应该做什么操作可以实现类似调用对应的类的构造方法的目的呢?

  • 通过调用类的无参构造创建对象

    • 字节码文件对象.newInstance(); 获取字节码文件对象后可通过newInstance(),调用无参构造函数快速创建对象,例如clazz.newInstance();

    • 获取对应的无参构造函数对象后通过constructor.newInstance(); 创建对象。

    • 若类的无参构造函数被私有后,无法通过字节码文件对象直接调用newInstance()方法创建对象,可以在获取构造函数对象后,设置过constructor.setAccessible(true)之后调用到私有构造函数。

  • 通过调用类的有参构造创建对象

    • 在获取构造函数对象后,通过constructor.newInstance('zs',18); 来创建对象。这种方式就等效于new Student('zs',18)

与构造对象相关的其他常用方法

  • constructor.getParameterTypes() 获取与当前构造函数对象相关的参数类型的字节码文件对象数组。

  • constructor.setAccessible(true) 暴力反射,进而调用私有构造函数。

  • constructor.getName() 获取当前构造函数的名称。

        @Test
        public void test() throws ClassNotFoundException, NoSuchMethodException {
             Class<?> clazz = Class.forName("com.itheima.bean.Student");
             //获取所有公有构造函数对象
            Constructor<?>[] constructors = clazz.getConstructors();
            Constructor<?> constructor1 = clazz.getConstructor(String.class, Integer.class);
            for (Constructor<?> constructor : constructors) {
                System.out.println("constructor.getName():"+constructor.getName());
                System.out.println("constructor.getParameterTypes():"
                        + Arrays.toString(constructor.getParameterTypes()));
            }
            //获取构造函数对象,包括公有和私有
            Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
            for (Constructor<?> declaredConstructor : declaredConstructors) {
                //设置私有构造可被调用,若无此行代码,私有构造函数对象将无法调用
                declaredConstructor.setAccessible(true);
            }
        }

反射之方法对象★★★★★

方法对象的获取

1. Method[] methods = clazz.getMethods(); //获取本类及父类的公有方法对象数组,然后可通过循环遍历获取方法对象。
2. Method[] declaredMethods = clazz.getDelaredMethods(); //获取本类的公有及私有方法对象 数组,然后可通过循环遍历获取方法对象。
3. Method method = clazz.getMethod(方法名,参数类型.class ...); //根据方法名及形参类型获取对应的方法对象。
  • 一定要注意:上述的第一种方法中获取的是本类及父类所有的公有方法对象,但是上述的第二种方法获取的只有本类的所有的公有与私有方法对象。

  • 上述的第三种方法总会有很多初学者不明白怎么去用,主要还是对方法理解不深刻导致的(应该对方法重载都不怎么理解吧),在Java中方法就算在同一个类中也是可以起重名的,那么,如何确定一个方法的决定性因素有哪些?方法名称、方法形参类型、方法形参个数、方法形参顺序(如果对重载理解的话,理解这块完全不是什么问题)。所以,在需要给出指定条件来获取到一个具体的方法对象时,需要给getMethod 方法提供方法名称,以及参数类型的字节码文件对象,比如说想要获取与public void findUser(String user,String password,int age){...} 方法对应的方法对象,那么需要这么写:Method method = clazz.getMethod("findUser",String.class,String.class,int.class) 即可。

方法对象的调用

  • 大家对于以下代码肯定不会陌生:

Student student = new Student();
student.setUsername("张三");
  • 如上,大家可以看出,当需要调用一个方法时,需要的必备条件:调用对象、被调用的方法、所 需传递的实参。那么,采用方法对象进行方法调用的时候也需要遵循这三点。

  • 另外若是私有方法,获取对应的方法对象后也是必须先对该方法对象做method.setAccessible(true) 处理后才可以执行方法的调用的。

  • 当通过getMethod() 方法获取对应的方法对象后,若要实现调用对应的方法,必须要给该方法提供调用该方法所有的对象以及需要往该方法里面传递的实参,于是,反射中调用方法的格式即:方法对象.invoke(调用对象,实参) 。具体示例看如下代码:

    Student student = new Student();
    //根据方法名及形参类型获取对应的方法对象
    Method setUsername = clazz.getMethod("setUsername", String.class);
    //方法对象.invoke(调用对象,实际参数值)
    setUsername.invoke(student, "张三");//就等效于student.setUsername("张三");

方法对象的其他常用方法

  • method.getParameterTypes() 获取与当前方法对象相关的参数类型的字节码文件对象数组。

  • method.setAccessible(true) 暴力反射,进而调用私有方法。

  • method.getName() 获取当前方法的名称。

反射之内省机制★★★

内省机制是通过反射来实现的,可以更方便的获取、调用类中的方法,属性。此处只对采用内省机制快速获取并操作类的getter、setter方法做简要说明与举例,不做深入讲解,若感兴趣,请结合JDK 的API文档查看IntrospectorBeanInfoMethodDescriptorPropertyDescriptor 等api的详细用法。

/**
 * 内省机制
 *
 * @author tengchao
 * @create 2017-12-10-20:09.
 */
public class beaninfo_demo {
    @Test
    public void test() throws IntrospectionException {
        /*
        这行代码主要的目的是在给定的禁止分析的断点(即Object.class)下,在 Java Bean 上进行内省,了解其所有属性和公开的方法。
        Student.class为要被分析的类,Object.class为停止分析的类,
        若是Student中含有与Object里面一样的方法、属性、事件,
        都将会停止分析。
        */
        BeanInfo beanInfo = Introspector.getBeanInfo(Student.class,Object.class);
        //获得 Student.class的MethodDescriptor对象数组
        MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
        for (MethodDescriptor methodDescriptor : methodDescriptors) {
            System.out.println(
                    "方法描述器对象对应的名字为:"+methodDescriptor.getName()
                            +"方法描述器对象对应的方法对象为:"+methodDescriptor.getMethod());
        }
        /*
        * 获得 Student.class的PropertyDescriptor对象数组
        * Property(属性)即getUsername或setUsername中get或set后面的那一部分首字母小写后叫做属性,而不是定义的字段名
        * */
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            System.out.println(
                    //即getXxx()方法
                    "属性描述器对应的getter方法为:"+propertyDescriptor.getReadMethod()
                            //即setXxx()方法
                            +"属性描述器对应的setter方法为:"+propertyDescriptor.getWriteMethod());
        }
    }
}

采用内省机制在获取一个类中的方法、属性、事件的时候会特别的方便,若是有这相关的需求,建议采用内省机制来做,上面的案例中包括了对内省机制的使用步骤及核心api的用法,强烈建议参照此例对内省机制做进一步的了解。

Last updated

Was this helpful?