反射
1.类加载机制
Java中的类加载机制是Java运行时的核心组成部分,它负责在程序运行过程中动态加载和连接类文件,并将其转换为可执行代码。这一机制遵循“按需加载”的原则,即只有在需要用到某个类的时候,才会将这个类的相关信息加载到内存中。
1.1.类的生命周期
Java中的类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一定在初始化之前完成,以支持Java的动态绑定。
1.2.类加载的时机
类加载的时机通常是在以下情况发生时:
-
主动引用:
- 创建类的实例(即使用new关键字)。
- 访问类的静态变量或静态方法。
- 反射(如使用
Class.forName()
)强制加载类。 - 初始化一个类的子类(会首先初始化子类的父类)。
- JVM启动时,包含main方法的类被加载。
-
被动引用:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组和集合,不会触发该类的初始化。
- 类A引用类B的静态常量(注意静态常量必须是字面值常量,否则还是会触发B的初始化)。
1.3.类加载的过程
类加载过程包括5个阶段:加载、验证、准备、解析、初始化。
-
加载阶段:
- 通过类的全限定名获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为Metaspace(元空间)区的运行时存储结构。
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为元空间区中该类各种数据的访问入口。
-
验证阶段:
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 包括文件格式验证、元数据验证、字节码验证和符号引用验证。
-
准备阶段:
- 为类的静态变量分配内存并设置初始值(通常是数据类型的零值,final修饰的静态变量除外)。
- 不包括实例变量的分配,实例变量将在对象实例化时分配。
-
解析阶段:
- 将常量池内的符号引用替换为直接引用的过程。
- 符号引用是以一组符号来描述所引用的目标,直接引用则是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
-
初始化阶段:
- 真正开始执行类中定义的Java程序代码。
- 初始化阶段是执行类构造器
<clinit>()
方法的过程。 <clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。
1.4.类加载器
Java中的类加载是通过类加载器(ClassLoader)及其子类来完成的。类加载器具有层次结构和加载顺序,遵循双亲委派模型。主要的类加载器包括:
-
启动类加载器(Bootstrap ClassLoader):
- 负责加载Java运行时核心类库,如
rt.jar
。 - 由C++实现,不是
ClassLoader
的子类,无法直接获取。
- 负责加载Java运行时核心类库,如
-
扩展类加载器(Extension ClassLoader):
- 负责加载Java虚拟机扩展目录(如
<JRE_HOME>/lib/ext
)中的类库。 - 继承自
java.net.URLClassLoader
。 - 开发者可以直接使用
- 负责加载Java虚拟机扩展目录(如
-
应用程序类加载器(Application Class Loader):
- 负责加载应用程序的类和资源文件,即classpath路径下的类。
- 继承自
java.lang.ClassLoader
。 - 开发者可以直接使用
-
自定义类加载器:
- 开发人员可以根据需要自定义类加载器,以实现特定的类加载策略。
2.什么是反射
Java中的反射(Reflection)是一种强大的机制,它允许程序在运行时(runtime)动态地访问和操作类的内部属性、方法以及构造器等。这种机制使得Java具有更高的灵活性和动态性。(new方法创建的对象是在编译时加载)
2.1.定义
Java反射是Java语言的一个重要特性,它使得Java程序在运行时能够访问、检测和修改它本身的结构和行为。通过反射,程序可以获取任意类的内部信息(如属性、方法、构造器等),并可以动态地创建对象、调用方法以及访问和修改对象的属性。
2.2.主要功能
- 获取类的信息:反射可以获取类的名称、包信息、所有属性、方法、注解、类型、类加载器等。
- 动态创建对象:通过反射可以在运行时动态地创建类的对象实例。
- 调用方法:反射允许程序在运行时调用对象的任意方法,包括私有方法。
- 访问和修改属性:反射还可以访问和修改对象的私有属性。
- 判断对象类型:通过反射可以判断任意一个对象所属的类。
2.3.使用场景
- 动态代理:反射可以用于实现动态代理,即在不修改原有代码的基础上,为接口或类添加额外的功能。这在AOP(面向切面编程)中非常有用。
- 框架开发:许多流行的Java框架(如Spring、Hibernate等)都利用了Java反射技术来实现对配置文件的解析、对注解的支持、对自定义类型的支持等功能。
- 性能优化:反射可以用于性能优化,例如在运行时动态加载类、创建对象、调用方法等,从而避免了在编译时进行这些操作所需的时间和资源。
- 工具开发:反射可以用于开发一些实用的工具类,如序列化和反序列化工具、ORM框架的通用数据访问层等。
- 热部署:反射可以用于实现热部署,即在不重启应用服务器的情况下,实时更新编译后的字节码文件。
- 单元测试:反射可以用于编写单元测试,通过反射可以动态地创建对象、调用方法、访问属性和修改字段值等,从而更方便地进行测试。
2.4.注意事项
- 性能问题:反射操作通常比直接代码调用要慢,因为它需要额外的步骤来获取类的信息并执行相应的操作。因此,在性能要求较高的场景中应谨慎使用反射。
- 安全性问题:反射机制可以访问类的私有属性和方法,这可能会破坏封装性并导致安全问题。因此,在使用反射时应确保代码的安全性。
- 可读性和可维护性问题:过度使用反射可能会使代码变得复杂和难以阅读和维护。因此,在使用反射时应权衡其带来的好处和代价。
3.反射的原理
Java反射的原理主要基于Java运行时环境(JRE)中的类加载机制和JVM(Java虚拟机)的能力,它允许程序在运行时动态地加载类、获取类的详细信息,并操作类或对象的属性和方法。
3.1.反射的主要步骤
- 获取Class对象:
- 可以使用Class.forName(String className)方法,通过类的全限定名来加载类并获取Class对象。
- 可以使用类名.class来获取Class对象,例如
String.class
。 - 还可以使用对象的getClass()方法来获取其对应类的Class对象,例如
String str = "hello"; Class<?> cls = str.getClass();
。
- 获取类的信息:
- 通过Class对象可以获取类的各种信息,如字段(Field)、方法(Method)、构造函数(Constructor)等。
- 这些信息是以对象的形式提供的,如Field对象代表类的字段,Method对象代表类的方法等。
- 操作类的属性和方法:
- 可以使用反射机制来动态地创建对象、调用方法、访问和修改类的属性等。
- 对于私有属性和方法,可以使用setAccessible(true)方法来关闭Java的访问检查,从而进行访问和修改。
3.2.原理概述
Java反射的原理是通过在JVM中加载类的字节码文件,并通过这些文件生成Class对象。Class对象作为反射操作的基础,提供了丰富的API来获取和操作类的内部信息。通过这些API,程序可以在运行时动态地加载类、创建对象、调用方法、访问和修改类的属性等。这种机制大大增强了Java的灵活性和可扩展性,但也需要注意其带来的性能开销和安全性问题。
4.Class对象
在Java中,每个类都有一个与之对应的Class对象。这个Class对象包含了类的所有信息,包括类的属性、方法、构造函数等。它是反射操作的基础,通过Class对象可以获取类的详细信息,并进行相应的操作。
4.1.三种获取Class对象的方式
在Java中,有三种主要的方式来获取一个类的Class
对象,这是进行反射操作的第一步。
-
使用
Class.forName(String className)
方法这是最常用的动态加载类的方式。你需要提供一个类的全限定名(包括包名)作为字符串参数。
Class.forName()
方法会加载与给定字符串名称相关的类,并返回该类的Class
对象。如果找不到该类,则会抛出ClassNotFoundException
。try { Class<?> cls = Class.forName("java.lang.String"); System.out.println(cls.getName()); // 输出 java.lang.String } catch (ClassNotFoundException e) { e.printStackTrace(); }
注意:这种方法会触发类的初始化。
简单灵活(反射推荐使用)
-
使用
.class
语法对于已知类型,可以直接使用类型名称后跟
.class
来获取其Class
对象。这种方式在编译时就已经确定了要操作的类,因此不会触发类的加载过程(除非这个类还没有被加载过),但会返回该类的Class
对象。Class<String> cls = String.class; System.out.println(cls.getName()); // 输出 java.lang.String
注意:这种方式不会触发类的初始化。
需要导入类包依赖太强
-
使用对象的
getClass()
方法如果你有一个类的实例,可以使用该实例的
getClass()
方法来获取它的Class
对象。这种方式非常直接,因为每个Java对象都有一个getClass()
方法,它返回表示该对象实际类的Class
对象。String str = "Hello, World!"; Class<?> cls = str.getClass(); System.out.println(cls.getName()); // 输出 java.lang.String
注意:这种方式也不会触发类的初始化(如果该类已经被加载并初始化过了)。
已经new了对象,没必要用反射
总结:
Class.forName()
是在运行时动态加载类并获取其Class
对象的方式,它可能会触发类的初始化。.class
语法是在编译时就确定了要操作的类,它不会触发类的加载(除非类还未被加载),但会返回类的Class
对象,且不会触发类的初始化。getClass()
方法是通过对象的实例来获取其Class
对象的方式,它也不会触发类的初始化(如果类已经加载并初始化过了)。
4.2.两种创建Class对象的方式
5.反射的优点和缺点
5.1.优点:
- 提高灵活性和扩展性:
- 反射允许程序在运行时动态地加载类、创建对象、调用方法、访问和修改类的属性等,这大大提高了程序的灵活性和扩展性。
- 开发者可以编写更通用的代码,这些代码可以在不知道具体类信息的情况下工作。
- 简化框架开发:
- 许多Java框架和库都利用反射来简化开发过程。例如,Spring框架使用反射来自动地管理对象的生命周期和依赖关系。
- 反射使得框架能够动态地处理类的加载和对象的创建,从而减少了开发者需要编写的样板代码。
- 增强的测试和调试能力:
- 在进行单元测试和集成测试时,反射可以用于模拟和验证类的行为,而无需修改类的源代码。
- 开发者可以使用反射来访问和修改类的私有属性和方法,以便进行深入的调试和测试。
5.2.缺点:
- 性能开销:
- 反射操作通常比直接代码调用要慢,因为它需要额外的步骤来获取类的信息并执行相应的操作。
- 反射调用涉及类型检查、安全检查等额外工作,这些都会增加程序的执行时间。
- 安全性问题:
- 反射机制可以访问类的私有属性和方法,这可能会破坏封装性并导致安全问题。
- 如果不当地使用反射,可能会暴露敏感信息或允许未授权的访问。
- 可读性和可维护性:
- 过度使用反射可能会使代码变得复杂和难以阅读,因为反射调用通常不如直接代码调用那样直观。
- 反射代码可能更难于调试和维护,因为开发者需要理解反射的工作原理以及它如何影响程序的执行流程。
- 依赖性问题:
- 反射依赖于类的具体实现,如果类的结构发生变化(如方法重命名、参数变更等),则使用反射的代码可能会失效。
- 这要求开发者在修改类时不仅要考虑直接调用这些类的代码,还要考虑可能通过反射调用这些类的代码。
6.反射的用途
6.1. 框架设计
在框架设计中,反射技术被广泛应用以实现解耦,提高框架的可扩展性和灵活性。框架通常需要在运行时动态地加载和使用各种类,而不需要在编译时知道这些类的具体信息。反射允许框架根据配置或约定动态地创建对象、调用方法、访问属性等,从而实现了高度的灵活性和可扩展性。
6.2. 单元测试
在单元测试中,反射技术可以用来访问类的私有或受保护成员,使得测试更加全面。由于Java的封装性,通常不能直接访问类的私有成员,但通过使用反射,可以在测试中绕过这些限制,对类的内部状态和行为进行验证。
6.3. 动态代理
反射技术还可以用来创建动态代理对象,实现AOP(面向切面编程)等功能。动态代理允许在运行时动态地创建一个实现了指定接口的代理对象,该代理对象可以在调用目标方法前后执行特定的逻辑,如事务管理、安全检查等。
6.4. 访问和修改类的属性及方法
通过反射,可以在运行时动态地访问和修改类的属性及方法,这对于一些需要动态操作对象属性的场景非常有用。例如,在开发一些通用的数据处理框架时,可能需要根据配置动态地访问对象的属性并进行处理。
6.5. 序列化和反序列化
许多Java序列化和反序列化工具都是基于反射机制实现的。通过反射,可以在运行时动态地获取对象的类型信息,并将其转换为可存储或传输的格式。反序列化时,再根据类型信息动态地创建对象并恢复其状态。
6.6. 插件系统
在开发插件系统时,反射也扮演着重要角色。插件系统通常需要在运行时动态地加载和使用插件,而不需要在编译时知道插件的具体信息。通过反射,可以实现插件的动态加载和卸载,以及插件之间的交互。
6.7. 跨语言交互
在某些情况下,反射还可以用于实现跨语言的交互。例如,在Java中通过JNI(Java Native Interface)调用本地代码时,可以利用反射机制在Java端动态地创建和管理本地代码所需的参数和返回值。
需要注意的是,虽然反射提供了强大的灵活性和扩展性,但它也带来了一定的性能开销和安全性风险。因此,在使用反射时应该权衡其利弊,确保在合理的场景下使用,并采取相应的措施来降低性能开销和安全风险。
标签:反射,Java,对象,动态,Class,加载 From: https://www.cnblogs.com/tubby233/p/18347305