Java核心知识体系5:反射机制详解
Java核心知识体系1:泛型机制详解
Java核心知识体系2:注解机制详解
Java核心知识体系3:异常机制详解
Java核心知识体系4:AOP原理和切面应用
1 介绍
无论是那种语言体系,反射都是必不可少的一个技术特征。从Java体系来说,很多常用的技术框架或多或少都使用到了反射技术,比如Spring、MyBatis、RocketMQ、FastJson 等等。反射技术强大而必要,在大多数框架中起到举足轻重的作用。所以,反射也是Java必不可少的核心技术之一。
接下来我们来看看反射的一些技术要点:
- 反射的概念(即什么是反射)?
- 反射的作用(它帮我们解决了哪些问题)?
- 反射的实现原理?
- 如何使用反射?
下面我就针对以上的疑问,一一来讲解。
1.1 反射是什么?
Java反射(Reflection)是Java语言的一个核心特性,它允许运行中的Java代码对自身进行自我检查,甚至修改自身的组件。具体来说,反射机制提供了在运行状态中,对于任意一个类,都能够了解这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法在Java中就叫做反射。
一句话总结:反射就是在运行时才具体知晓要操作的类是什么结构,并在运行时获取类的完整构造,并调用对应的方法、属性等。
Java的反射主要包括以下三个部分:
- 类的加载:Java的类在需要使用时才会被加载到JVM中。这个过程是由类加载器(ClassLoader)完成的。类加载器首先检查这个类是否已经被加载过,如果还没有加载,那么就会从磁盘上加载类的字节码并创建一个Class对象。
- 获取类的信息:当一个对象被创建后,我们可以使用反射来获取这个对象的Class对象。通过这个Class对象,我们可以获取到这个类的所有属性和方法。
- 方法的调用:通过反射,我们可以动态的调用一个对象的方法。即使这个方法是一个私有的方法,也能够通过反射来调用。
1.2 为什么要用反射?
Java Reflection功能非常强大,并且非常有用,比如:
- 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
- 获取任意对象的属性,并且能改变对象的属性
- 调用任意对象的方法
- 判断任意一个对象所属的类
- 实例化任意一个类的对象
- 通过反射我们可以实现动态装配,降低代码的耦合度,实现动态代理等。
具体的应用场景:
- 框架设计:许多框架,如Spring,Hibernate等,都大量使用了反射来实现对象的自动装配,动态代理等功能。
- 单元测试:单元测试框架(如JUnit)会使用反射来调用被注解的方法。
- 插件化:为了实现插件化,可以通过反射加载不同的插件。
- 对象序列化与反序列化:在对象进行序列化和反序列化的时候,会使用反射获取到对象的所有属性和方法。
2 反射的使用
在Java中,Class类与java.lang.reflect类库配合对反射技术进行了完整的支持。在反射的Package中,我们经常使用功能类如下:
- Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象
- Field类表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)
- Method类表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)
下面将对这几个类进行详细介绍。
2.1 反射创建类对象
一般情况下我们通过反射创建类对象主要有两种方式:
-
通过 Class 对象的 newInstance() 方法
-
通过 Constructor 对象的 newInstance() 方法
-
通过 Class 对象的 newInstance() 方法实现
Class clz = Class.forName("com.ad.reflection.TestRefle");
TestRefle tr= (TestRefle)clz.newInstance();
- 通过 Constructor 对象的 newInstance() 方法实现
Class clz = Class.forName("com.ad.reflection.TestRefle");
Constructor constructor = clz.getConstructor();
TestRefle tr= (TestRefle)constructor.newInstance();
这边需要注意,通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。
下面的代码演示的是通过 Constructor 调用有参构造方法进行了类对象初始化:
Class clz = Class.forName("com.ad.reflection.TestRefle");
Constructor constructor = clz.getConstructor(String.class);
TestRefle tr= (TestRefle)constructor.newInstance("提供一个String参数");
接下来我们继续,通过具体的API获取详细的类信息:类信息、方法信息、属性信息等。
2.2 获取Class类对象
// 获取Class对象的三种方式
根据类名: Class mailClass = MailInfo.class;
根据对象: Class mailClass = new MailInfo().getClass();
根据全限定类名: Class mailClass = Class.forName("com.ad.MailInfo");
// 根据对象获取信息和实例对象
获取全限定类名: mailClass.getName();
获取类名: mailClass.getSimpleName();
实例化: userClass.getDeclaredConstructor().newInstance();
更加详细Class类获取参考如下:
方法 | 用途 |
---|---|
forName() | (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。 (2)为了产生Class引用,forName()立即就进行了初始化。 |
Object-getClass() | 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。 |
getName() | 取全限定的类名(包括包名),即类的完整名字。 getSimpleName() 获取类名(不包括包名) |
getCanonicalName() | 获取全限定的类名(包括包名) |
isInterface() | 判断Class对象是否是表示一个接口 |
getInterfaces() | 返回Class对象数组,表示Class对象所引用的类所实现的所有接口。 |
getSupercalss() | 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。 |
newInstance() | 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法创建的类,必须带有无参的构造器。 |
2.3 获取类的成员变量的信息
Field[] fields = _class.getDeclaredFields();
更加详细成员变量获取参考如下:
方法 | 用途 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获取所有的公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对象(public和非public) |
getDeclaredFields() | 获得所有属性对象(public和非public) |
2.4 获得类方法
Method[] methods = _class.getDeclaredMethods();
更加详细方法获取参考如下:
方法 | 用途 |
---|---|
getMethod(String name, Class...<?> paramerterTypes) | 获得某个公有的方法对象 |
getMethods() | 获取所有的公有的方法对象 |
getDeclaredMethod(String name, Class...<?> paramerterTypes) | 获得对应类下某个方法(public和非public) |
getDeclaredMethods() | 获得对应类下所有方法(public和非public) |
2.5 获得构造函数
Constructor[] constructors = _class.getDeclaredConstructors();
更加详细构造函数获取参考如下:
方法 | 用途 |
---|---|
getConstructor(Class...<?> paramerterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获取该类的所有公有构造方法 |
getDeclaredConstructor(Class...<?> paramerterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获取该类的所有构造方法 |
这样通过反射就可以做在运行时获取类的完整构造,并获得类信息了。
类名 | 用途 |
---|---|
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量(即类的属性) |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造函数 |
通过上面的几个示例我们基本了解了反射的使用,但这仅仅是使用,我们还需深入理解反射背后的底层实现原理。
3 反射原理分析
3.1 反射的调用流程
1、编写完Java项目之后,java文件都会被编译成一个.class文件
2、这些class文件在程序运行时会被ClassLoader加载到JVM中,当一个类被加载以后,JVM就会在内存中自动产生一个Class对象。
3、通过Class对象获取 Field(属性)、Method(方法)、Construcor(构造函数)
我们平时通过new的形式创建对象,本质上就是通过Class来创建个新对象
通过上面的流程我们可以看出反射的优势:
- 动态装配
我们的程序在运行时,可能不一定会用到所有我们编写和构建的类,这样避免启动时间太长并且浪费大量无用的机器资源。
取而代之的是动态的加载一些类,这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载。
- 降低耦合
如果你在使用new时明确的指定类名,那这就是典型的硬编码实现,而在使用反射的时候,可以只传入类名参数,就可以生成对象,降低了耦合度,使得程序更具灵性。
完整的调用流程,图片来自网上,比较模糊,后续再补一个
3.2 反射的应用场景
- 框架设计:许多框架,如Spring,Hibernate,mybatis,dubbo,rocketmq等,都大量使用了反射来实现对象的自动装配,动态代理等功能。
- 单元测试:单元测试框架(如JUnit)会使用反射来调用被注解的方法。
- 插件化:为了实现插件化,可以通过反射加载不同的插件。
- 对象序列化与反序列化:在对象进行序列化和反序列化的时候,会使用反射获取到对象的所有属性和方法。
- 动态配置、动态代理:通过反射去读取配置,以及代理请求
4 反射经典案例解析
以下案例来自百度文心一言大模型自动生成,已调试通过。
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取目标类的Class对象
Class<?> targetClass = Class.forName("java.util.ArrayList");
// 获取目标类的所有公共方法
Method[] methods = targetClass.getMethods();
// 遍历所有方法并打印方法名
for (Method method : methods) {
System.out.println(method.getName());
}
// 获取特定方法,比如添加元素的add方法
Method addMethod = targetClass.getMethod("add", Object.class);
// 创建目标类的实例对象
Object targetObject = targetClass.newInstance();
// 调用add方法添加元素
addMethod.invoke(targetObject, "Hello, World!");
// 获取目标类的所有属性(字段)并打印属性名
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个案例展示了如何使用反射来获取目标类的Class对象,获取并打印目标类的所有公共方法,获取特定方法,创建目标类的实例对象,调用目标类的方法,以及获取并打印目标类的所有属性(字段)。
总结
无论是那种语言体系(C#、Java等等),反射都是必不可少的一个技术特征。而从Java体系来说,很多常用的技术框架或多或少都使用到了反射技术,比如Spring、MyBatis、RocketMQ、FastJson 等等。
学习好Java 反射技术能帮助你更好的理解底层调用的原理,也有助于设计更加 轻巧、高内聚、低耦合 的业务框架。