【通用开发】Java反射

【通用开发】Java反射

本文介绍了Java反射机制的使用和关于其速度的测试结论

简单来说,Java 反射(Reflection)就是一种让 Java 程序在运行时能够“看清”并操作自身的能力。

通常,我们编写 Java 代码时,在编译阶段就已经确定了类、方法、字段等信息。但反射打破了这种限制,它允许你在程序运行时:

  1. 获取类的信息: 比如,一个对象属于哪个类?这个类有哪些字段(属性)?有哪些方法?有哪些构造函数?等等。
  2. 操作类的成员:
    • 创建对象: 不通过 new 关键字,而是动态地创建类的实例。
    • 访问/修改字段: 即使是 private 的字段,也能读取或修改它的值。
    • 调用方法: 即使是 private 的方法,也能动态地调用它。

反射主要用于:

  • 框架和库的开发: 很多流行的 Java 框架(如 Spring、Hibernate、JUnit)都大量使用了反射。它们需要在运行时动态地加载类、注入依赖、调用方法等,而不需要提前知道用户会定义哪些具体的类。
  • 动态代理: 在不修改原有代码的情况下,为对象增加新的功能。
  • 序列化和反序列化: 当对象需要保存到文件或网络传输时,需要知道对象的内部结构。
  • 单元测试工具: 允许测试框架访问私有成员进行测试。

反射也可以叫自省,向内探查。那些外部访问不到的API,可以通过反射强行调用。如果编译时知道类或对象的具体信息,此时直接对类和对象正常初始化操作即可,无需使用反射(reflection)。如果编译不知道类或对象的具体信息,就要用到 反射 来实现。比如类的名称放在XML文件中,属性和属性值放在XML文件中,需要在运行时读取XML文件,动态获取类的信息。Web领域对动态扩展的要求很高,会大量用到反射。

场景

Java反射机制的核心是在程序运行时 动态加载类并获取类的详细信息 ,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。

Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。

在编译时根本无法知道该对象或类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息比如:log4j,Servlet、SSM框架技术都用到了反射机制。

Android平台上,LayoutInflator解析xml利用了反射生成view,还有EventBus使用反射进行了解耦处理等。

使用

使用反射创建对象,调用方法举例:


public class ReflectionExample {
    private static final String TAG = "ReflectionExample";
    public static void init() {
        try {
            // 获取类对象
            Class<?> clazz = Class.forName("com.stephen.commondemo.alltest.MyClass");
            // 获取构造函数
            Constructor<?> constructor = clazz.getConstructor();
            // 使用构造函数创建对象
            Object obj = constructor.newInstance();
            // 获取方法
            Method method = clazz.getMethod("myMethod", String.class);
            // 调用方法
            method.invoke(obj, "Hello, Reflection!");
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
                 InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    private static final String TAG = "MyClass";
    public MyClass() {
        Log.i(TAG, "MyClass instance created.");
    }
    public void myMethod(String message) {
        Log.i(TAG, "Method called with message: " + message);
    }
}

优缺点

1、优点:

在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

2、缺点:

(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射; (2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

反射获取Class信息

反射的关键实现方法有以下几个:

  • 得到类:Class.forName(“类名”)
  • 得到所有字段:getDeclaredFields()
  • 得到所有方法:getDeclaredMethods()
  • 得到构造方法:getDeclaredConstructor()
  • 得到实例:newInstance()
  • 调用方法:invoke()

例如现在有一个Human类,设置几个参数,构造函数,公共方法。

package com.stephen.commondemo.alltest;

public class Human {
    private static final String TAG = "Human";
    public String gender;
    public String age;

    public Human(String gender, String age) {
        this.gender = gender;
        this.age = age;
    }

    private Human() {
    }

    public Human(String gender) {
        this.gender = gender;
    }

    public String getGender() {
        return gender;
    }

    public String getAge() {
        return age;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

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

    public void eat() {
        System.out.println("Human is eating.");
    }

    public void speak(String str) {
        System.out.println("Human is speaking" + str);
    }
}

在另一个类,使用反射获取类的信息。

package com.stephen.commondemo.alltest;

import android.util.Log;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;

public class HumanInfoGetter {

    private static final String TAG = "HumanInfoGetter";

    public static void init() throws Exception {
        // 1.获取一个类的结构信息(类对象 Class对象)
        Class<?> clazz = Class.forName("com.stephen.commondemo.alltest.Human");
        // 2.从类对象中获取类的各种结构信息
        // 2.1 获取基本结构信息
        Log.i(TAG, clazz.getName());
        Log.i(TAG, clazz.getSimpleName());
        Log.i(TAG, Objects.requireNonNull(clazz.getSuperclass()).getName());
        Log.i(TAG, Arrays.toString(clazz.getInterfaces()));
        // 2.2 获取构造方法
        // 只能得到public修饰的构造方法
        // Constructor[] constructors = clazz.getConstructors();
        // 可以得到所有的构造方法
        Constructor[] constructors = clazz.getDeclaredConstructors();
        Log.i(TAG, constructors.length + "");
        for (Constructor con : constructors) {
            // System.out.println(con.toString());
            Log.i(TAG, con.getName() + "||" +
                    Modifier.toString(con.getModifiers()) + "  ||"
                    + Arrays.toString(con.getParameterTypes()));
        }
        // Constructor con = clazz.getConstructor();// 获取无参数构造方法
        // Constructor con = clazz.getConstructor(String.class,String.class);
        Constructor<?> con =
                clazz.getDeclaredConstructor(String.class, String.class);
        Log.i(TAG, String.valueOf(con));
        // 2.3 获取属性
        // Field[] fields = clazz.getFields();
        Field[] fields = clazz.getDeclaredFields();
        Log.i(TAG, String.valueOf(fields.length));
        for (Field f : fields) {
            Log.i(TAG, String.valueOf(f));
        }
        // Field f = clazz.getField("color");
        // private 默认 protecte public都可以获取,但不包括父类的
        Field f = clazz.getDeclaredField("age");
        Log.i(TAG, String.valueOf(f));
        // 2.3 获取方法
        // Method[] methods = clazz.getMethods();
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            Log.i(TAG, String.valueOf(m));
        }

        Method m1 = clazz.getMethod("speak", String.class);
        Method m2 = clazz.getDeclaredMethod("eat");

        Log.i(TAG, String.valueOf(m1));
        Log.i(TAG, String.valueOf(m2));
    }
}

原理

从上述内容可以看出,对于反射来说,操纵类最主要的方法是 invoke,所以搞懂了 invoke 方法的实现,也就搞定了反射的底层实现原理了。

invoke 方法的执行流程如下:

  1. 查找方法:当通过 java.lang.reflect.Method 对象调用 invoke 方法时,Java 虚拟机(JVM)首先确认该方法是否存在并可以访问。这包括检查方法的访问权限、方法签名是否匹配等。
  2. 安全检查:如果方法是私有的或受保护的,还需要进行访问权限的安全检查。如果当前调用者没有足够的权限访问这个方法,将抛出 IllegalAccessException。
  3. 参数转换和适配:invoke 方法接受一个对象实例和一组参数,需要将这些参数转换成对应方法签名所需要的类型,并且进行必要的类型检查和装箱拆箱操作。
  4. 方法调用:对于非私有方法,Java 反射实际上是通过 JNI(Java Native Interface,Java 本地接口)调用到 JVM 内部的 native 方法,例如 java.lang.reflect.Method.invoke0()。这个 native 方法负责完成真正的动态方法调用。对于 Java 方法,JVM 会通过方法表、虚方法表(vtable)进行查找和调用;对于非虚方法或者静态方法,JVM 会直接调用相应的方法实现。
  5. 异常处理:在执行方法的过程中,如果出现任何异常,JVM 会捕获并将异常包装成 InvocationTargetException 抛出,应用程序可以通过这个异常获取到原始异常信息。
  6. 返回结果:如果方法正常执行完毕,invoke 方法会返回方法的执行结果,或者如果方法返回类型是 void,则不返回任何值。

通过这种方式,Java 反射的 invoke 方法能够打破编译时的绑定,实现运行时动态调用对象的方法,提供了极大的灵活性,但也带来了运行时性能损耗和安全隐患(如破坏封装性、违反访问控制等)。

反射为什么比正常加载慢

简单来说,因为 反射需要在运行时动态获取类的信息 ,这比在编译时就获取信息要慢。

反射性能低么?为什么?

  • 反射调用过程中会产生大量的临时对象,这些对象会占用内存,可能会导致频繁 gc,从而影响性能。
  • 反射调用方法时会从方法数组中遍历查找,并且会检查可见性等操作会耗时。
  • 反射在达到一定次数时,会动态编写字节码并加载到内存中,这个字节码没有经过编译器优化,也不能享受JIT优化。

  • 反射一般会涉及自动装箱/拆箱和类型转换,都会带来一定的资源开销。

经过方法调用的测试,反射比正常调用大约慢100倍。反射方法调用耗时大约是 0.0004ms ,而Android屏幕刷新率是60-120hz,每一帧的耗时大概8.3ms到16ms之间,如果要使用户感受到反射带来的卡顿,至少要17000多次调用。

除了循环之外,不会有这么多的反射调用。

所以反射虽然慢,在非高频的场景下,正常使用完全没有问题。

反射慢流传的原因

由于时代原因,在 Android 4.4 及之前的设备上,反射的耗时大约为0.008-0.09ms,大概慢了 20-300 倍。取个100倍。

按照每一帧16ms来算,给每一帧分配10%的时间片留给反射,那只有41次的调用机会了。

在Android5.0推出了ART,性能优化了一大截。