• Java 反射详解:动态创建实例、调用方法和访问字段


    “一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 new 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。”

    Writer writer = new Writer();
    writer.setName("少年");
    
    
    • 1
    • 2
    • 3

    像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 new 关键字创建对象了。

    我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为反射。

    Class clazz = Class.forName("com.itwanger.s39.Writer");
    Method method = clazz.getMethod("setName", String.class);
    Constructor constructor = clazz.getConstructor();
    Object object = constructor.newInstance();
    method.invoke(object,"少年");
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    像上面这个例子,就可以理解为“反射”。“反射的缺点主要有两个。”

    • 破坏封装:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。
    • 性能开销:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。

    反射的主要应用场景有:

    • 开发通用框架:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
    • 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
    • 注解:注解本身只是起到一个标记符的作用,它需要利用发射机制,根据标记符去执行特定的行为。

    举例:

    public class Writer {
        private int age;
        private String name;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    测试类:

    public class ReflectionDemo1 {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Writer writer = new Writer();
            writer.setName("少年");
            System.out.println(writer.getName());
    
            Class clazz = Class.forName("com.itwanger.s39.Writer");
            Constructor constructor = clazz.getConstructor();
            Object object = constructor.newInstance();
    
            Method setNameMethod = clazz.getMethod("setName", String.class);
            setNameMethod.invoke(object, "少年");
            Method getNameMethod = clazz.getMethod("getName");
            System.out.println(getNameMethod.invoke(object));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    结果:

    少年
    少年
    
    • 1
    • 2

    只不过,反射的过程略显曲折了一些。

    第一步,获取反射类的 Class 对象:

     Class clazz = Class.forName("com.itwanger.s39.Writer");
    
    • 1

    在 Java 中,Class 对象是一种特殊的对象,它代表了程序中的类和接口。

    Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。

    Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。

    除了前面提到的,通过类的全名获取 Class 对象,还有以下两种方式:

    • 如果你有一个类的实例,你可以通过调用该实例的getClass()方法获取 Class 对象。例如:String str = “Hello World”; Class cls = str.getClass();
    • 如果你有一个类的字面量(即类本身),你可以直接获取 Class 对象。例如:Class cls = String.class;
      第二步,通过 Class 对象获取构造方法 Constructor 对象:
    Constructor constructor = clazz.getConstructor();
    
    • 1

    第三步,通过 Constructor 对象初始化反射类对象

    Object object = constructor.newInstance();
    
    • 1

    第四步,获取要调用的方法的 Method 对象:

    Method setNameMethod = clazz.getMethod("setName", String.class);
    Method getNameMethod = clazz.getMethod("getName");
    
    • 1
    • 2

    第五步,通过 invoke() 方法执行:

    setNameMethod.invoke(object, "少年");
    getNameMethod.invoke(object)
    
    • 1
    • 2

    要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。

    也就是说,java.lang.Class 是所有反射 API 的入口。

    而方法的反射调用,最终是由 Method 对象的 invoke() 方法完成的,来看一下源码(JDK 8 环境下)。

    public Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException,
            InvocationTargetException {
        // 如果方法不允许被覆盖,进行权限检查
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                // 检查调用者是否具有访问权限
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        // 获取方法访问器(从 volatile 变量中读取)
        MethodAccessor ma = methodAccessor;
        if (ma == null) {
            // 如果访问器为空,尝试获取方法访问器
            ma = acquireMethodAccessor();
        }
        // 使用方法访问器调用方法,并返回结果
        return ma.invoke(obj, args);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    两个嵌套的 if 语句是用来进行权限检查的。

    invoke() 方法实际上是委派给 MethodAccessor 接口来完成的。

    在这里插入图片描述
    MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。
    在这里插入图片描述

    • NativeMethodAccessorImpl:通过本地方法来实现反射调用;
    • DelegatingMethodAccessorImpl:通过委派模式来实现反射调用;
      通过 debug 的方式进入 invoke() 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。
      在这里插入图片描述

    也就是说,invoke() 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。

    也就是说,invoke() 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。

    “之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。
    那临界点是多少呢?“默认是 15 次。“可以通过 -Dsun.reflect.inflationThreshold 参数类调整。”

    来看下面这个例子。

    Method setAgeMethod = clazz.getMethod("setAge", int.class);
    for (int i = 0;i < 20; i++) {
        setAgeMethod.invoke(object, 18);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在 invoke() 方法处加断点进入 debug 模式,当 i = 15 的时候,也就是第 16 次执行的时候,会进入到 if 条件分支中,改变 DelegatingMethodAccessorImpl 的委派模式 delegate 为 (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(),而之前的委派模式 delegate 为 NativeMethodAccessorImpl。
    在这里插入图片描述
    “接下来,我们再来熟悉一下反射当中常用的 API。”

    1)获取反射类的 Class 对象
    Class.forName(),参数为反射类的完全限定名

    Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3");
    System.out.println(c1.getCanonicalName());
    
    Class c2 = Class.forName("[D");
    System.out.println(c2.getCanonicalName());
    
    Class c3 = Class.forName("[[Ljava.lang.String;");
    System.out.println(c3.getCanonicalName());
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出结果:

    com.itwanger.s39.ReflectionDemo3
    double[]
    java.lang.String[][]
    
    
    • 1
    • 2
    • 3
    • 4

    类名 + .class,只适合在编译前就知道操作的 Class。

    Class c1 = ReflectionDemo3.class;
    System.out.println(c1.getCanonicalName());
    
    Class c2 = String.class;
    System.out.println(c2.getCanonicalName());
    
    Class c3 = int[][][].class;
    System.out.println(c3.getCanonicalName());
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    来看一下输出结果:

    com.itwanger.s39.ReflectionDemo3
    java.lang.String
    int[][][]
    
    
    • 1
    • 2
    • 3
    • 4

    2)创建反射类的对象

    • 用 Class 对象的 newInstance() 方法。
    • 用 Constructor 对象的 newInstance() 方法
    Class c1 = Writer.class;
    Writer writer = (Writer) c1.newInstance();
    
    Class c2 = Class.forName("com.itwanger.s39.Writer");
    Constructor constructor = c2.getConstructor();
    Object object = constructor.newInstance();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3)获取构造方法

    Class 对象提供了以下方法来获取构造方法 Constructor 对象:

    • getConstructor():返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
    • getDeclaredConstructor():返回反射类的特定构造方法,不限定于 public 的。
    • getConstructors():返回类的所有 public 构造方法。
    • getDeclaredConstructors():返回类的所有构造方法,不限定于 public 的。
    Class c2 = Class.forName("com.itwanger.s39.Writer");
    Constructor constructor = c2.getConstructor();
    
    Constructor[] constructors1 = String.class.getDeclaredConstructors();
    for (Constructor c : constructors1) {
        System.out.println(c);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4)获取字段

    大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。

    Method setNameMethod = clazz.getMethod("setName", String.class);
    Method getNameMethod = clazz.getMethod("getName");
    
    • 1
    • 2

    5)获取方法

    大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。

    Method[] methods1 = System.class.getDeclaredMethods();
    Method[] methods2 = System.class.getMethods();
    
    • 1
    • 2

    注意,如果想反射访问私有字段和(构造)方法的话,需要使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。

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

  • 相关阅读:
    UDP可靠性传输-QUIC
    HTML5:七天学会基础动画网页10
    Nginx配置ssl证书(https证书)
    深入浅出落地应用分析:AI数字人「微软小冰」
    深度学习基础--神经网络(3)神经网络的学习,损失函数初识
    爱上开源之golang入门至实战-第二章语言基础-变量
    光路科技:工业以太网交换机引领工业互联网新篇章
    JAVA生成20位LONG型UUID
    凭什么软件测试入门就有一万+工资,为什么?我不相信。
    搜维尔科技:通过Haption力反馈在无接触仿真中操纵虚拟机器人
  • 原文地址:https://blog.csdn.net/zch981964/article/details/136462935