美文网首页
Java中的反射技术

Java中的反射技术

作者: 胜舟 | 来源:发表于2021-01-18 21:57 被阅读0次

    反射技术是java的核心技术之一,虽然我们日常开发中,基本上可能用的并不多,但是它同样也是必学的。因为很多框架的设计其实都是有利用反射机制的,这意味着反射是我们向前迈进的一个重要技术。

     

    一、反射是什么?

    首先我们看一段普通的调用代码

    先创建一个平平无奇的Person类,有一个平平无奇的work方法

    package com.lzh.reflect;
    
    
    public class Person {
        public void work(String content) {
            System.out.println(content);
        }
    }
    

    再创建个Test类进行调用

    public class Test {
        public static void main(String[] args) {
            Person person = new Person();
            person.work("编码中");
        }
    }
    

    结果不用贴了,只是将字符串“编码中”,输出到了控制台。

     

    然后现在我们利用反射机制,做相同的调用操作:

    public class Test {
        public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, ClassNotFoundException {
            Class clz = Class.forName("com.lzh.reflect.Person");
            Method method = clz.getMethod("work",String.class);
            Constructor constructor = clz.getConstructor();
            Object object = constructor.newInstance();
            method.invoke(object,"编码中");
        }
    }
    

    ??

    别看这一坨代码比较臃肿复杂,但是它的执行效果和前面的普通调用其实是一样的。只是它是通过反射去调用的,最明显的区别就是,反射在调用同一个类的同一个方法时,并没有指定类。听起来有点绕,但是如果你的Person和Test两个文件在不同的包下,就可以看出来,普通的调用必须import com.lzh.reflect.Person;,导入Person类。但如果是反射,就不需要导入具体的包,而是通过运行时分析参数里的类路径字符串判断出是哪个类。

    站在编码者的角度,即便用了反射,我们也知道是用了哪个类,调用了哪个方法,但是对于jvm来说,它们不运行到这一步是不会知道的。在编译时,类的路径名、方法名对它来说只是个字符串参数而已。

    通过反射我们甚至可以通过修改配置或传特定的参数,无需重启服务器,就能更改服务器内部的调用逻辑,所以这点也是一个安全隐患,在使用反射时一定要小心使用外部输入的参数。

     

    总结:普通调用在编译后,就知道了要加载哪个类,调用这个类的哪个方法。但是反射是在运行时才知道要操作的类是什么,并且在运行时获取类的某个构造方法构建对象,然后调用类的某个方法。

     

     

    二、怎么使用反射

    1.获取Class

    首先我们需要通过反射获取一个类的对象

    ①Class.forName(类的全路径) 静态方法

    第一种方式,就是我们前面例子用的方法了。使用Class.forName(类的全路径) 指定类,这种方式我们需要知道类的全路径名,如果路径有误加载不到类,会抛出ClassNotFoundException异常。

    Class clz = Class.forName("com.lzh.reflect.Person");
    
    ②.class方法

    这种方法是需要导入类的,指定了类名,看起来好像有点脱裤子放屁没啥用,因为反射不就是因为不能确定类才用反射吗。但是如果我们重点关注的是反射调用哪个方法或哪个构造函数,而Class其实是已经确定的,所以并不需要在Class上费时间时,是可以用到这种方式的。

    Class clz = Person.class;
    
    ③对象的getClass()方法

    这种方式就更简单了,直接通过对象获取类,比方式②还要具体,这次连构造方法都无需关注了。同样,可能在我们只关注method的时候可以用上。

    Person person = new Person();
    Class clz1 = person.getClass();
    
    ④基本类型的Type属性

    如果是基本类,我们可以通过对应包装类的TYPE属性,获取它基本类的Class,每个基本类型都有这个TYPE属性。

    Class byteClass = Byte.TYPE;
    Class shortClass = Short.TYPE;
    Class integerClass = Integer.TYPE;
    Class longClass = Long.TYPE;
    Class charClass = Character.TYPE;
    Class floatClass = Float.TYPE;
    Class doubleClass = Double.TYPE;
    Class boolClass = Boolean.TYPE;
    或简便写成
    Class byteClass = byte.class;
    Class shortClass = short.class;
    Class integerClass = int.class;
    Class longClass = long.class;
    Class charClass = char.class;
    Class floatClass = float.class;
    Class doubleClass = double.class;
    Class boolClass = boolean.class;
    

    也可以简便的写成int.class、byte.class这样,效果是一样的,通过反编译可以看到int.class最后实际上也是Integer.TYPE。

    但是注意,这并不等于Integer.class,基本类型的Class类型和包装类的Class类型是两码事,它们并不是一个东西

    ⑤类Class

    我们也可以在获取了子类Class对象后,通过getSuperclass()获取它的父类Class。

    如下,Student的父类是Person:

    Student student = new Student();
    Class slz = student.getClass();
    Class superclass = slz.getSuperclass();
    

     

    2.获取对象

    获取了Class后,我们自然需要通过某个构造方法获取这个类的对象

    ①通过Class对象的newInstance()方法

    直接通过类获取实例,但是原理是调用无参构造方法,所以如果这个类没有无参构造方法,会抛出InstantiationException异常。

    Class clz = Person.class;
    Person person = (Person) clz.newInstance();
    
    ②通过Constructor 对象的 newInstance() 方法

    如果我们想调用指定的构造方法,就得用这种方式了。先给Person类随便加一个有参的构造方法:

    public class Person {
        //有参构造方法
        public Person(String name) {
            System.out.println(name);
        }
        public void work(String content) {
            System.out.println(content);
        }
    }
    

    然后通过clz.getConstructor(String.class) 获取指定参数的构造器(如果是获取无参构造就不传参数),这里参数也是类的Class,最后通过构造器的newInstance()方法调用构造方法,同时这里需要传入前面声明的参数:

    Class clz = Person.class;
    Constructor constructor = clz.getConstructor(String.class);
    Person person = (Person) constructor.newInstance("123");
    

    注意这里的newInstance()方法是构造器Constructor对象中的方法,和前面的Class中的不是一个方法,所以也会抛出更多种类的异常。

    ③私有构造方法

    如果这个构造方法是私有的呢,给工具人Person类加一个私有的构造方法:

        private Person(Integer age) {
            System.out.println(age);
        }
    

    如果还是用前面的getConstructor(Integer.class);就会发现抛出了NoSuchMethodException异常,找不到这个私有的构造方法,这是因为普通的getConstructor只能获取到public级别的构造方法

    这时候我们只需要将getConstructor()替换成getDeclaredConstructor()方法即可,其实就是加了个关键字Declared,它可以读取到所有已声明的构造方法,这样即便是私有的也可以找到。另外还需要额外设置一下setAccessible为true,关闭反射对象的访问检查,不然也是无法访问私有构造方法的。

    Class clz = Person.class;
    Constructor constructor = clz.getDeclaredConstructor(Integer.class);
    constructor.setAccessible(true);
    Person person = (Person) constructor.newInstance(18);
    

     

    3.获取成员变量

    首先还是掏出工具人Person类,给它上两个变量,其中一个设为了私有。同时为了看结果,我重新生成了toString方法。

    public String name;
    private Integer age;
    
    
    @Override
    public String toString() {
    return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
    
    ①通过getField()方法获取成员变量

    通过getField()获取指定的成员变量,可以通过getName获取成员变量的名字,getType获取成员变量的类型

    Class clz = Person.class;
    Field field = clz.getField("name");
    System.out.println(fields.getName());//获取名字
    System.out.println(fields.getType());//获取类型
    

    如果要获取值,则需要传入一个对象才行,然后直接调用get方法:

    //初始化一个Person对象,并赋给成员变量一个值
    Person person = new Person();
    person.name = "张三";
    
    
    //通过反射取出这个成员变量的值
    Class clz = person.getClass();
    Field field = clz.getField("name");//指定field是name变量
    System.out.println(field.get(person));//获取传入对象person的name成员变量值
    
    ②通过field.set()为对象设置成员变量值

    和get的使用方法一样,set也要传入一个Person对象,不然它怎么知道是给哪个对象设置值呢?这里我们试着用反射构造一个对象,再反射set变量属性后反射get出来:

    Class clz = Person.class;    //声明Class
    Constructor clzConstructors = clz.getConstructor();    //获取默认的无参构造器
    Object person = clz.newInstance();//初始化person对象
    
    
    //获取成员属性,并set值进去
    Field field = clz.getField("name");
    field.set(person, "马德");
    //输出对象的属性
    System.out.println(field.get(person));
    

    有人可能觉得你用了同一个Field对象set完了再get,当然get的到啊。实际上就算你在get的步骤前重新初始化一个Field对象去get,结果也是一样的。field并不会绑定在某一个对象上。

    ③私有成员变量

    和构造器一样,只需要加上Declared,用getDeclaredField() ,就可以获取私有级别的成员变量。记住,没有加Declared的方法都是只能访问public级别的,不管是构造器还是成员变量还是方法。

    并且也都需要setAccessible(true);关闭访问级别的检查:

    Field field = clz.getDeclaredField("age");
    field.setAccessible(true);
    field.set(person, 18);
    System.out.println(field.get(person));
    

     

    4.获取和调用方法

    最后终于到核心了,利用反射调用方法

    ①通过getMethod()获取方法,invoke调用方法

    这里我偷了个懒,直接把最前面文章开头的例子拿过来了,方法还是普通的work方法

        public void work(String content) {
            System.out.println(content);
        }
    

    这次,我们应该能看懂这个调用了

    Class clz = Class.forName("com.lzh.reflect.Person");
    Method method = clz.getMethod("work",String.class);    //获取work(String)方法
    Constructor constructor = clz.getConstructor();    //获取构造器
    Object object = constructor.newInstance();    //获取对象实例
    method.invoke(object,"编码中");    //调用object对象的work方法,并传入参数
    

    首先通过clz.getMethod("work",String.class); 获取了work方法,第二个包括以后的参数代表work方法的参数,有几个就要填几个,不然如果work方法有多个重载,反射无法找到具体的方法。

    最后调用时是用method.invoke(object,"编码中"); 方法,第一个参数是要执行哪个对象的work方法,和field对象的getset方法类似,第二个包括以后的参数就是work方法所需的参数了,数量要对上,不然会抛出IllegalArgumentException异常。

    ②调用私有方法

    估计大家都猜到怎么调用私有了,getDeclaredMethod() 可以访问到所有方法,包括私有方法。记得如果没加Declared,就只能查到public级别的。所以一般我们用getDeclaredMethod就行,稳妥。

    并且要记得method.setAccessible(true)。

    然后我们整个最完整的反射,没有import导入Person类,完整的调用一次方法

    Person类:

    public class Person {
       
        public String name;
        private Integer age;    //私有属性年龄
       
        public Person(String name) {    //公共有参构造方法
            this.name = name;
        }
       
        private String play(String game) {        //私有有参方法,且用到了成员变量
            return this.name+"("+this.age+") "+"play ♂ " + game;
        }
    
    
    }
    

    调用的测试方法:

    //通过反射获取Class
    Class clz = Class.forName("com.lzh.reflect.Person");
    
    
    //获取有参构造器,那个初始化同时会设置name的构造器
    Constructor constructor = clz.getDeclaredConstructor(String.class);
    
    
    //获取实例,并初始化了name属性
    Object person = constructor.newInstance("比利");
    
    
    //手动获取age成员属性,并设置值
    Field field = clz.getDeclaredField("age");
    field.setAccessible(true);
    field.set(person, 28);
    
    
    //调用方法
    Method method = clz.getDeclaredMethod("play", String.class);
    method.setAccessible(true);
    Object invoke = method.invoke(person,"摔跤");
    System.out.println(invoke);
    

    这次放一次输出结果:

    比利(28) play ♂ 摔跤
    

     

    *5.一些其他的方法

    再简单说明和反射相关的几个其他方法,非必读,可选择性跳过。

    ①获取构造器、方法、变量加s

    Class对象中,我们已经知道了有getConstructor()和getDeclaredConstructor()获取构造器,getMethod()和getDeclaredMethod()是获取方法,getField()和getDeclaredField()是获取属性。

    但是如果在这些方法名的尾部加个s,就代表获取全部的构造器/方法/属性,

    返回类型是:Constructor或Method或Field类型的数组

    Constructor[] clzConstructors = clz.getConstructors();
    Constructor[] declaredConstructors = clz.getDeclaredConstructors();
    
    
    Field[] fields = clz.getFields();
    
    
    Method[] methods = clz.getMethods();
    
    ②getModifiers()

    Constructor、Method、Field对象都有方法getModifiers(),返回的是一个整数值,它代表当前的方法或属性前面的修饰符相加得到的值:无任何修饰符是0 , public是1 ,private是2 ,protected是4,static是8 ,final是16。例如一个方法前面的修饰符是public static final ,那么它的值就是相加得到25。

    返回类型是:int

    这个方法虽然只返回一个数字,但是通过计算是一定能准确的得到修饰符的,因为都是2的倍数,所以每个返回的数字只会有计算出一种结果。

    而且我们也没必要计算,反射有提供Modifier.toString() 方法,帮我们把这个数字转换为修饰符:

    System.out.println("修饰符: "+Modifier.toString(method.getModifiers()));
    
    ③getParameterTypes()

    Constructor、Method对象的方法getParameterTypes(),返回当前方法的参数列表,是数组格式的Class列表。

    返回类型是:Class<?>[]

    System.out.println(Arrays.toString(work.getParameterTypes()));
    

     

     

    三、总结

    至此,只要认真的看完并自己尝试后,我们对于反射就算没入门,也至少会用了。

    我也是工作了一年后才开始学习的反射,因为工作中没怎么用过,呃,应该说就没用过。

    但是当我们查看一些牛逼框架代码的源码时,就会发现里面很多地方有用到反射,就拿我们耳熟能详的Spring来说,它的核心IOC和AOP功能也是依靠了反射的。所以就算只是为了看源码,为了应付面试官,我们也应当学会反射,学了反射大家都是人上人。

     

     

     

    参考资料:

    大白话说Java反射:入门、使用、原理

    https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html

    Java反射技术详解

    https://blog.csdn.net/huangliniqng/article/details/88554510

    相关文章

      网友评论

          本文标题:Java中的反射技术

          本文链接:https://www.haomeiwen.com/subject/pjttzktx.html