【Java基础系列】- 反射机制
文章目录
一、程序运行过程
ClassLoader找到主类将其Load到内存中的CodeSegment,运行环境找到main方法并开始执行,运行过程中会有更多class被load到内存
-
ClassLoader:负责将Java字节码(.class文件)加载到JVM中,具有双亲委派机制
-
Class:表示Java中的类和接口,通过反射机制可以动态地获取类的信息和操作类的成员
二、ClassLoader
站在classloader角度来看,CodeSegment中的每个.class就是一个class对象
ClassLoader的类加载机制
- 并非一次性加载到内存,需要时加载(运行期间动态加载)
- static语句块在加载后无论new几次只执行一次
- dynamic语句块每次new新对象都会执行,等同于构造方法中语句
JDK ClassLoader的层次关系
类加载器是以双亲委派模型工作的,确保了类加载的安全性和稳定性:
- 当一个类加载器需要加载一个类时,它首先将这个请求委托给它的父类加载器
- 如果父类加载器找不到这个类,它再尝试自己加载
!!不是继承,继承指的是类之间的关系,在这说的是对象之间的关系
-
application class loader
内有一个引用parent指向extesion class loader
,extesion class loader
内有一个引用指向bootstrap class loader
,即指向其parent对象,通过getParent()方法获取 -
application class loader
加载用户自定义类时会先找到其parent的class loader询问其该类有没有被加载,如果已经有同名的被加载了,则该自定义类就不会被加载(双亲委派机制)
ClassLoader分类
ClassLoader
是一个抽象类,它的主要作用是将Java字节码加载到Java虚拟机(JVM)中。Java中有三种主要的类加载器:
-
启动类加载器(Bootstrap ClassLoader):
-
加载JDK的核心类,把其他classloader加载进来
-
负责加载核心Java类库,如
java.lang
、java.util
等。它是用本地代码实现的,是虚拟机的一部分
-
-
扩展类加载器(Extension ClassLoader):
- 负责加载扩展类库,默认加载
JAVA_HOME/lib/ext
目录下的所有类库
- 负责加载扩展类库,默认加载
-
应用程序类加载器(Application ClassLoader):
- 负责加载应用程序类路径(classpath)上的类,用户自定义类,它是最常用的类加载器
常用方法
ClassLoader
类提供了一些常用的方法:
loadClass(String name)
: 加载一个类getParent()
: 获取父类加载器getResource(String name)
: 查找资源文件getResources(String name)
: 查找所有匹配的资源文件
自定义类加载器:
有时需要自定义类加载器来加载某些特殊来源的类。如,从网络或加密的JAR文件中加载类:
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassData(name); // 自定义加载类数据的逻辑
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassData(String name) {
// 加载类数据的逻辑
return new byte[0]; // 示例代码
}
}
三、Class
Class
类是一个非常重要的类,它表示正在运行的Java应用程序中的类和接口
每个类或接口在JVM中都有一个Class
对象与之对应,这个Class
对象在类加载时由JVM自动创建
获取Class对象的三种方式
-
通过类名的
.class
属性:Class<?> clazz = String.class;
-
通过对象的
getClass()
方法:String str = "Hello"; Class<?> clazz = str.getClass();
-
通过
Class.forName()
方法:Class<?> clazz = Class.forName("java.lang.String");
常用方法
Class
类提供了许多方法来反射地获取类的信息:
getName()
: 获取类的全限定名。getSuperclass()
: 获取父类的Class
对象。getInterfaces()
: 获取实现的接口。getDeclaredFields()
: 获取所有声明的字段。getDeclaredMethods()
: 获取所有声明的方法。getDeclaredConstructors()
: 获取所有声明的构造方法。newInstance()
: 创建类的新实例(已过时,推荐使用Constructor.newInstance()
)。
以下是一个使用Class
类和反射获取类信息的示例:
public class ReflectionExample {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("Class Name: " + clazz.getName());
System.out.println("Superclass: " + clazz.getSuperclass().getName());
System.out.println("Interfaces: ");
for (Class<?> iface : clazz.getInterfaces()) {
System.out.println(iface.getName());
}
System.out.println("Methods: ");
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(method.getName());
}
System.out.println("Fields: ");
for (Field field : clazz.getDeclaredFields()) {
System.out.println(field.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
四、反射
-
反射机制使java变成动态语言,允许程序在执行过程中借助Reflection API取得任何类的内部信息,
-
允许程序在运行时检查和修改自身的结构和行为
-
通过反射,可以在运行时动态地获取类的信息、创建对象、调用方法和访问字段(内部属性)
-
反射主要用于框架、库和工具开发,但也可以用于调试和测试,使得程序在运行时能够动态地操作类和对象
-
它也有其局限性和开销
加载完类之后,堆内存中就产生了一个Class类对象,一个类只有一个Class对象,通过该对象可以看到类的结构:
- 正常方式:引入需要的包类名称->通过new实例化->取得实例化对象
- 反射方式:实例化对象->getClass()方法->得到完整的包类名称
主要类和方法
反射主要依赖于以下几个类和接口:
- Class类:
- 获取类的字节码信息
forName(String className)
: 获取指定类名的Class对象newInstance()
: 创建Class对象所表示的类的一个新实例
- Constructor类:
- 表示类的构造方法
newInstance(Object... initargs)
: 创建新实例
- Method类:
- 表示类的方法
invoke(Object obj, Object... args)
: 调用该方法
- Field类:
- 表示类的字段,静态常量
get(Object obj)
: 获取字段的值set(Object obj, Object value)
: 设置字段的值
以下是一些基本的反射操作示例:
获取类的Class对象
Class<?> clazz = Class.forName("com.example.MyClass");
创建类的实例
Object obj = clazz.newInstance();
获取并调用方法
Method method = clazz.getMethod("myMethod", String.class);
method.invoke(obj, "Hello, World!");
获取并设置字段的值
Field field = clazz.getDeclaredField("myField");
field.setAccessible(true); // 如果字段是私有的,需要设置可访问性
field.set(obj, "New Value");
获取构造方法并创建实例
Constructor<?> constructor = clazz.getConstructor(String.class);
Object obj = constructor.newInstance("Constructor Argument");
注意事项
- 性能:反射操作通常比直接调用慢,因为它涉及动态类型检查。
- 安全性:反射可以绕过正常的访问控制(例如访问私有字段和方法),这可能会导致安全问题。
- 代码可维护性:过度使用反射会使代码难以理解和维护。
使用场景
- 框架开发:例如Spring和Hibernate使用反射来动态管理和注入依赖。
- 序列化和反序列化:例如Gson和Jackson通过反射将对象转换为JSON格式,反之亦然。
- 单元测试:例如JUnit可以通过反射调用私有方法进行测试。
五、反射带来的动态性
反射使Java具备了动态语言的一些特性。虽然Java是一种静态类型语言(即类型检查发生在编译时),但反射提供了在运行时动态操作类和对象的能力。这使得Java可以在某些方面表现得像动态语言。以下是反射如何使Java具备动态语言特性的几个方面:
通过反射,Java可以在运行时加载和使用未知的类。这使得程序可以根据运行时条件加载不同的类,而不是在编译时确定所有依赖关系。
Class<?> clazz = Class.forName("com.example.SomeClass");
Object obj = clazz.newInstance();
反射允许在运行时调用方法,而不需要在编译时知道方法的确切签名。这样可以实现更加灵活的代码调用。
Method method = clazz.getMethod("someMethod", String.class);
method.invoke(obj, "Hello");
反射使得可以在运行时访问和修改对象的字段,即使这些字段在编译时未知。
Field field = clazz.getDeclaredField("someField");
field.setAccessible(true);
field.set(obj, "New Value");
可以在运行时选择并调用构造函数,创建新的对象实例。
Constructor<?> constructor = clazz.getConstructor(String.class);
Object newInstance = constructor.newInstance("Constructor Argument");
Java反射还支持动态代理(Dynamic Proxy),允许在运行时创建实现一个或多个接口的对象。这对于AOP(面向方面编程)和拦截器模式非常有用。
InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[]{MyInterface.class},
handler);