反射
反射的概念
- Java反射是指在运行时检查或修改类、方法、字段等程序结构的能力。
- 通过Java反射,可以在运行时获取类的信息、调用方法、访问字段,以及动态创建对象实例,而无需在编译时知道这些信息。
- 这使得Java程序可以在运行时动态地操作类和对象,从而实现更灵活的编程方式。
- 当程序在运行时,所有的类、对象、方法等信息都被加载到内存中。通过反射,程序可以在运行过程中访问和修改这些信息。
反射实现的过程
- 获取Class对象
- 在Java中,要使用反射,首先需要获取要操作的类的Class对象。有三种常用的方式可以获取Class对象:
- 通过对象的getClass()方法: 使用对象的getClass()方法可以获取对应类的Class对象。
- 通过类名.class: 使用类名.class语法可以获取对应类的Class对象。
- 通过Class.forName()方法: 使用Class.forName(“类的完整路径”)方法可以根据类的完整路径获取对应类的Class对象。
- 获取类的构造函数、方法和字段
- 一旦有了Class对象,就可以通过反射获取类的构造函数、方法字段等信息:
- 获取构造函数: 使用getConstructors()方法可以获取类的所有公有构造函数,使用getDeclaredConstructors()方法可以获取类的所有构造函数(包括私有构造函数)。
- 获取方法: 使用getMethods()方法可以获取类的所有公有方法,使用getDeclaredMethods()方法可以获取类的所有方法(包括私有方法)。
- 获取字段: 使用getFields()方法可以获取类的所有公有字段,使用getDeclaredFields()方法可以获取类的所有字段(包括私有字段)。
- 实例化对象、调用方法和访问字段
- 通过反射,可以实例化对象、调用方法和访问字段:
- 实例化对象: 使用Constructor类的newInstance()方法可以实例化对象。
- 调用方法: 使用Method类的invoke()方法可以调用类的方法。
- 访问字段: 使用Field类的get()和set()方法可以访问和修改类的字段值。
- 动态代理
- Java反射机制还可以实现动态代理。通过Proxy类和InvocationHandler接口,可以动态地创建代理对象,实现对目标对象的代理操作。
- 注意事项
- 使用反射机制需要注意性能问题,因为反射操作相对于直接调用方法或访问属性来说会更慢一些。
- 反射机制破坏了封装性,可以访问和修改类的私有方法和字段,因此在使用反射时需要慎重考虑安全性和设计合理的访问控制。
//反射的实例
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
public class ReflectionExample {
private String privateField = "私有字段";
public String publicField = "公共字段";
private ReflectionExample() {
System.out.println("私有构造函数被调用");
}
public ReflectionExample(int num) {
System.out.println("公共构造函数被调用,参数为 " + num);
}
private void privateMethod() {
System.out.println("私有方法被调用");
}
public void publicMethod() {
System.out.println("公共方法被调用");
}
public static void main(String[] args) throws Exception {
// 获取类的 Class 对象
Class<?> clazz = ReflectionExample.class;
// 获取私有字段并设置其值
Field privateField = clazz.getDeclaredField("privateField");
//覆盖字段、方法和构造函数的默认访问限制,从而访问和修改私有成员。
privateField.setAccessible(true);
privateField.set(new ReflectionExample(), "修改后的私有字段值");
// 获取公共字段并读取其值
Field publicField = clazz.getField("publicField");
System.out.println("公共字段的值: " + publicField.get(new ReflectionExample()));
// 获取私有构造函数并实例化对象
Constructor<?> privateConstructor = clazz.getDeclaredConstructor();
privateConstructor.setAccessible(true);
privateConstructor.newInstance();
// 获取带参数的公共构造函数并实例化对象
Constructor<?> publicConstructor = clazz.getConstructor(int.class);
publicConstructor.newInstance(42);
// 调用私有方法
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(new ReflectionExample());
// 调用公共方法
Method publicMethod = clazz.getMethod("publicMethod");
publicMethod.invoke(new ReflectionExample());
}
}
Class类
- 在Java中,Class 类是一个特殊的类,它代表了一个 Java 类的实例。
- Class 类的对象包含了关于特定类的结构信息,例如类的字段、方法、构造函数等。
- 通过 Class 类,可以获取有关类的各种信息,并在运行时动态地操作类。
- 通过 Class 类,可以实现反射(Reflection)机制,使得在运行时可以动态地获取类的信息、调用方法等
用java类的加载理解Class类
- 针对编写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码文件。
- 接着,我们使用java.exe命令对指定的.class文件进行解释运行。
- 这个解释运行的过程中,我们需要将.class字节码文件加载到内存中
- 加载到内存中的.class文件对应的结构即为Class的一个实例
- 比如:Class myClass = MyClass.class;中的MyClass.class其实是内存里面的一个运行时类
- 运行时类在内存中会缓存起来,在整个执行期间,只会加载一次
获取类的 Class 对象:
Class<?> myClass = MyClass.class; // 通过类名获取 Class 对象
Class<?> myClass = obj.getClass(); // 通过对象实例获取 Class 对象
Class<?> myClass = Class.forName("com.example.MyClass"); // 通过类的全限定名获取 Class 对象
获取类的信息:
String className = myClass.getName(); // 获取类的名称
Field[] fields = myClass.getDeclaredFields(); // 获取类的字段
Method[] methods = myClass.getDeclaredMethods(); // 获取类的方法
创建类的实例:
MyClass obj = (MyClass) myClass.newInstance(); // 使用无参构造函数创建实例
调用类的方法:
Method method = myClass.getMethod("methodName", parameterTypes);
method.invoke(obj, args);
类的加载过程
- 加载(Loading):
- 加载是指查找并加载类的字节码文件。当程序中使用到某个类时,Java虚拟机会通过类加载器(ClassLoader)来加载这个类。
- 加载阶段包括以下步骤:
- 通过类的全限定名(Fully Qualified Name)获取类的二进制字节流。
- 将字节流转换为方法区中的运行时数据结构。
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
- 链接(Linking):
- 链接阶段又分为验证(Verification)、准备(Preparation)、解析(Resolution)三个步骤:
- 验证:确保被加载的类满足Java虚拟机规范,比如格式验证、语义验证、字节码验证等。
- 准备:为类的静态变量分配内存空间,并设置默认初始值。
- 解析:将类、接口、字段和方法的符号引用解析为直接引用。
- 初始化(Initialization):
- 在初始化阶段,Java虚拟机会对类进行初始化,包括执行类构造器
<clinit>()
方法。 - 类的初始化是按需进行的,即在首次主动使用类时才会进行初始化,主动使用包括创建类的实例、访问类的静态变量、调用类的静态方法等。
类加载器
类加载器的工作流程
- 加载(Loading):查找并加载类的二进制数据。
- 链接(Linking):
- 验证(Verification):确保加载的类符合Java虚拟机规范。
- 准备(Preparation):为类的静态变量分配内存并设置默认初始值。
- 解析(Resolution):将符号引用解析为直接引用。
- 初始化(Initialization):执行类构造器方法 (),也就是静态代码块。
双亲委派模型(Parent Delegation Model)
- 双亲委派模型是类加载器的一种组织结构,其核心思想是:当一个类加载器收到加载类的请求时,它会先委托给父类加载器去加载,只有在父类加载器无法加载时,才会自己尝试加载。这种模型的优点在于保证Java核心类库不会被篡改,同时避免类的重复加载。
类加载器的分类
- 引导类加载器(Bootstrap Class Loader):负责加载核心Java类,由C++实现,无法直接获取。
- 扩展类加载器(Extension Class Loader):负责加载Java的扩展类,通常加载$JAVA_HOME/jre/lib/ext目录下的类。
- 应用程序类加载器(Application Class Loader):负责加载应用程序中的类,是Java应用程序默认的类加载器。
- 自定义类加载器(Custom Class Loader):开发者可以根据需要编写自定义的类加载器,实现特定的加载行为。
类加载器的委托机制
- 当一个类加载器收到加载类的请求时,它会先询问父类加载器是否能够加载该类。
- 如果父类加载器无法加载,则该类加载器尝试加载类。
- 这个过程一直持续,直到达到顶层的引导类加载器。
类加载器的破坏
- 破坏双亲委派模型:破坏双亲委派模型可能导致类的重复加载和安全性问题。
- 破坏类加载器的唯一性:如果两个类加载器加载同一个类,会导致类的类型不一致,可能引发类型转换异常等问题。