反射详解
前言
大家好,我是Leo哥🫣🫣🫣,今天一起来回顾一下反射机制。
什么是反射
反射(Reflection) 是编程语言中的一种能力,它允许程序在运行时动态地检查和操作程序元素,比如类、方法、字段等。通俗地说,反射就是程序能够在运行时查看自己并做出相应调整的能力。
如果了解过框架底册的话,那么对反射一定不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
假设你有一辆汽车,通常你需要知道汽车的型号、颜色等信息来驾驶它。但如果汽车具备 反射 能力,它自己就能告诉你它的型号、颜色,甚至你可以通过特定的方法直接改变它的某些属性。编程中的反射就是这样一种机制,让程序能够动态地查看和修改自身。
整理了一份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
反射的场景
平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring、Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
其实反射有许多具体的应用场景的。
反射是一种强大的工具,它允许程序在运行时检查和操作类的结构,包括类的属性、方法和构造函数。反射有许多具体的应用场景,以下是一些常见的应用场景和示例:
1. 动态对象创建
反射可以根据类名在运行时创建对象实例。这对于工厂模式或需要根据配置文件创建实例的场景非常有用。
示例代码:
java
复制代码
Class<?> clazz = Class.forName("com.example.MyClass"); Object instance = clazz.getDeclaredConstructor().newInstance();
2. 动态方法调用
反射允许在运行时调用对象的方法,这在需要根据配置或用户输入决定调用哪个方法时特别有用。
示例代码:
java
复制代码
Method method = clazz.getMethod("myMethod", String.class); method.invoke(instance, "Hello");
3. 访问和修改私有字段
反射可以绕过访问控制机制访问和修改私有字段。这对于调试、框架开发和需要操作内部状态的场景非常有用。
示例代码:
java
复制代码
Field field = clazz.getDeclaredField("privateField"); field.setAccessible(true); field.set(instance, "newValue");
4. 框架和库的实现
许多流行的框架和库(如 Spring、Hibernate)使用反射来实现依赖注入、对象关系映射和自动化测试等功能。
Spring 示例:
Spring 使用反射来自动注入依赖项,例如:
java
复制代码
@Autowired private MyService myService;
Spring 在运行时使用反射来查找和注入 MyService
的实例。
5. 动态代理
Java 动态代理机制基于反射,允许在运行时创建接口的代理实例。动态代理常用于 AOP(面向切面编程)和拦截器模式。
示例代码:
java
复制代码
InvocationHandler handler = new MyInvocationHandler(targetObject); MyInterface proxy = (MyInterface) Proxy.newProxyInstance( targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), handler );
其实注解中也大量使用到了反射机制,例如,我们比较熟悉的Spring框架,我们通过使用一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?
这些都是因为反射机制,获取到类/属性/方法/方法的参数上的注解。你获取到注解之后就可以做进一步处理了。
反射的基础
简单来说,反射就是为把Java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
这里我们首先需要理解 Class类,以及类的加载机制。 然后基于此我们如何通过反射获取Class类以及类中的成员变量、方法、构造方法等。
认识Class类
在 Java 反射中,Class
类是核心部分。它表示正在运行的 Java 应用程序中的类或接口。每个类或接口都有一个与之对应的 Class
对象。当 JVM 加载某个类时,它会创建一个 Class
对象来表示这个类。
Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation)(每个Java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取Class对象)。
通过Class的源码,我们可以得出以下结论:
- Class类也是类的一种,与class关键字是不一样的。
Class<T>
类实现了多个接口,包括Serializable
、GenericDeclaration
、Type
和AnnotatedElement
,并且不能被继承(使用了final
关键字)。- 定义了三个私有静态常量
ANNOTATION
、ENUM
和SYNTHETIC
,分别用于标识注解、枚举和合成类。 - Class类有一个私有构造函数,只有JVM能够调用,用于初始化
Class
对象,防止默认构造函数的生成。 - 私有构造函数通过参数
ClassLoader
初始化classLoader
字段,确保其非空以避免JIT优化问题。
认识类加载
类加载机制
Java类的加载过程包括加载、验证、准备、解析、初始化、使用和卸载七个步骤。加载阶段将类字节码加载到内存中,验证阶段确保类的字节码符合规范,准备阶段为静态变量分配内存,解析阶段将符号引用转换为直接引用,初始化阶段执行类的初始化代码,使用阶段允许类实例化和使用,最后在卸载阶段回收类的内存。
类的加载流程
首先就是编译过程。Java源代码文件(如Person.java
和Car.java
)被编译成字节码文件(如Person.class
和Car.class
)。
然后就是进入了类加载,类加载器(Class Loader) 负责加载字节码文件。字节码文件可以来自本地文件系统、网络等不同来源。
在方法区中,类加载器将加载的类字节码文件存储到方法区中,并为每个类创建一个对应的数据结构(如Person类数据结构
和Car类数据结构
)。
在堆区中,JVM在堆区中为每个类创建一个唯一的Class
对象(如Person Class对象
和Car Class对象
),用于表示该类的元数据。
最后,当程序创建类的实例时(如Person
类的实例P1
、P2
等,和Car
类的实例C1
、C2
等),这些实例对象在堆区中被分配内存。每个实例对象通过其Class
对象获取类的元数据,从而实现反射机制,即可以获取类的内部信息(如方法、属性、构造函数等)以及反向控制实例对象的能力。
通过下面这张图你能更清晰的了解到类加载机制。
学会使用反射
获取Class对象的几种方式
要通过反射获取具体的信息,那么首先需要获取到Class对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。
1. 通过类名(静态方式)
这是获取 Class
对象最常用和最简单的一种方式,适用于在编译时已知类的情况下。
java
复制代码
public class MyClass { // 类的定义 } public class Main { public static void main(String[] args) { Class<?> clazz = MyClass.class; System.out.println("Class name: " + clazz.getName()); } }
- 使用
MyClass.class
可以直接获取MyClass
的Class
对象。 - 这种方式在编译时检查类的存在和合法性,如果类不存在或拼写错误,编译时就会报错。
2. 通过对象的 getClass()
方法(动态方式)
当你已经有一个类的实例对象时,可以通过该对象的 getClass()
方法获取其 Class
对象。
typescript
复制代码
public class MyClass { // 类的定义 } public class Main { public static void main(String[] args) { MyClass obj = new MyClass(); Class<?> clazz = obj.getClass(); System.out.println("Class name: " + clazz.getName()); } }
- 使用实例对象的
getClass()
方法可以获取该对象的Class
对象。 - 这种方式适用于在运行时已经有类实例的情况下。
3. 通过 Class.forName()
方法(动态加载)
这种方式适用于需要在运行时根据类名字符串动态加载类的场景。
typescript
复制代码
public class Main { public static void main(String[] args) { try { Class<?> clazz = Class.forName("com.example.MyClass"); System.out.println("Class name: " + clazz.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
- 使用
Class.forName("com.example.MyClass")
可以动态加载MyClass
类。 - 这种方式适用于在运行时需要根据类名字符串加载类的场景,例如配置文件中指定类名。
4. 通过类加载器(ClassLoader)
这种方式用于需要自定义类加载器的场景,例如在开发插件系统或模块化系统时。
csharp
复制代码
public class Main { public static void main(String[] args) { try { ClassLoader classLoader = Main.class.getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.MyClass"); System.out.println("Class name: " + clazz.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
说明:
- 使用类加载器的
loadClass()
方法可以加载指定类。 - 这种方式适用于需要自定义类加载逻辑的场景。
API基本使用
java
复制代码
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; public class ReflectionExample { public static void main(String[] args) { try { // 获取类的 Class 对象 Class<?> clazz = Class.forName("com.example.MyClass"); // 获取类名 System.out.println("Class Name: " + clazz.getName()); // 获取修饰符 int modifiers = clazz.getModifiers(); System.out.println("Is Public: " + Modifier.isPublic(modifiers)); System.out.println("Is Abstract: " + Modifier.isAbstract(modifiers)); // 获取父类 Class<?> superClass = clazz.getSuperclass(); System.out.println("Superclass: " + superClass.getName()); // 获取实现的接口 Class<?>[] interfaces = clazz.getInterfaces(); System.out.println("Interfaces:"); for (Class<?> iface : interfaces) { System.out.println(iface.getName()); } // 获取字段 Field[] fields = clazz.getDeclaredFields(); System.out.println("Fields:"); for (Field field : fields) { System.out.println(field.getName() + " - " + field.getType().getName()); } // 获取方法 Method[] methods = clazz.getDeclaredMethods(); System.out.println("Methods:"); for (Method method : methods) { System.out.println(method.getName() + " - " + method.getReturnType().getName()); } // 获取构造函数 Constructor<?>[] constructors = clazz.getDeclaredConstructors(); System.out.println("Constructors:"); for (Constructor<?> constructor : constructors) { System.out.println(constructor.getName()); } // 创建实例 Constructor<?> constructor = clazz.getConstructor(); Object instance = constructor.newInstance(); System.out.println("Instance: " + instance); // 调用方法 Method method = clazz.getMethod("methodName", String.class); method.invoke(instance, "Hello"); // 访问和修改字段 Field field = clazz.getDeclaredField("fieldName"); field.setAccessible(true); field.set(instance, "newValue"); System.out.println("Field Value: " + field.get(instance)); } catch (Exception e) { e.printStackTrace(); } } }
反射的基本原理
首先我们编写的 Java 源文件(.java)被保存在磁盘上。然后,这些源文件通过 Java 编译器(javac)编译生成字节码文件(.class)。在编译过程中,编译器会检查源文件中的语法错误,如果存在语法错误,将无法生成字节码文件。接着,JVM 将这些字节码文件加载到内存中。
Java 是面向对象的编程语言,所有事物都可以被看作对象。当字节码文件被加载到内存中时,JVM 会将其表示为一个 Class 对象。这个 Class 对象包含了与类有关的所有信息。
一旦获得了 Class 对象,就可以通过反射机制操作类中的所有内容。Java 将类的成员变量、构造器、成员方法都看作对象,并将其封装到相应的类中。通过 Class 对象,可以调用相应的方法获取类的成员变量对象、构造器对象、成员方法对象。然后,通过这些对象调用方法,就相当于该类在操作自身的属性、构造器和成员方法。这就是反射的基本原理。
每个类在编译后都会生成一个同名的 .class 文件,该文件保存着 Class 对象的内容。类加载实际上是 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。反射提供了运行时获取类信息的能力,允许在运行时加载类,即使在编译时期该类的 .class 文件不存在,也可以在运行时加载并使用反射机制操作。