【通用开发】Java反射

本文介绍了Java反射机制的使用和关于其速度的测试结论
简单来说,Java 反射(Reflection)就是一种让 Java 程序在运行时能够“看清”并操作自身的能力。
通常,我们编写 Java 代码时,在编译阶段就已经确定了类、方法、字段等信息。但反射打破了这种限制,它允许你在程序运行时:
- 获取类的信息: 比如,一个对象属于哪个类?这个类有哪些字段(属性)?有哪些方法?有哪些构造函数?等等。
- 操作类的成员:
- 创建对象: 不通过
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 方法的执行流程如下:
- 查找方法:当通过 java.lang.reflect.Method 对象调用 invoke 方法时,Java 虚拟机(JVM)首先确认该方法是否存在并可以访问。这包括检查方法的访问权限、方法签名是否匹配等。
- 安全检查:如果方法是私有的或受保护的,还需要进行访问权限的安全检查。如果当前调用者没有足够的权限访问这个方法,将抛出 IllegalAccessException。
- 参数转换和适配:invoke 方法接受一个对象实例和一组参数,需要将这些参数转换成对应方法签名所需要的类型,并且进行必要的类型检查和装箱拆箱操作。
- 方法调用:对于非私有方法,Java 反射实际上是通过 JNI(Java Native Interface,Java 本地接口)调用到 JVM 内部的 native 方法,例如 java.lang.reflect.Method.invoke0()。这个 native 方法负责完成真正的动态方法调用。对于 Java 方法,JVM 会通过方法表、虚方法表(vtable)进行查找和调用;对于非虚方法或者静态方法,JVM 会直接调用相应的方法实现。
- 异常处理:在执行方法的过程中,如果出现任何异常,JVM 会捕获并将异常包装成 InvocationTargetException 抛出,应用程序可以通过这个异常获取到原始异常信息。
- 返回结果:如果方法正常执行完毕,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,性能优化了一大截。