继上一篇博客【注解和反射】什么时候类会和不会被初始化?-CSDN博客
目录
六、类加载器
测试:获得类加载器
(1)如何获取Java中的类加载器及其父类加载器
(2)测试当前类是哪个类加载器
(3)获取系统类加载器可以加载的路径
双亲委派机制
自己如果写一个自己的String类,路径是java.lang.String,可以吗?
六、类加载器
类加载器(ClassLoader)是Java运行时环境(JRE)的一部分,它负责在运行时动态地加载Java类到Java虚拟机(JVM)中。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。
类加载器的主要作用包括:
- 加载类:根据类的全名(包括包名)找到对应的.class文件,并将其加载到JVM中。加载的方式分为隐式加载和显示加载两种。隐式加载指的是程序在使用new等方式创建对象时,会隐式地调用类的加载器把对应的类加载到JVM中。
- 链接:这个过程包括验证、准备和解析三个步骤。验证是确保被加载的类的正确性和安全性;准备是为类的静态变量分配内存,并将其初始化为默认值;解析是把类中的符号引用转换为直接引用。
- 初始化:为类的静态变量赋予正确的初始值。
此外,Java中有三种主要的类加载器,它们分别是:
- 启动类加载器(Bootstrap ClassLoader):这是JVM自带的类加载器,负责加载Java的核心类库,如rt.jar等。
- 扩展类加载器(Extension ClassLoader):负责加载Java的扩展类库,它的父加载器是Bootstrap。
- 系统类加载器(System ClassLoader):也称为应用类加载器(Application ClassLoader),负责加载应用程序classpath目录下的所有类。它的父加载器是Extension ClassLoader。
类加载器采用双亲委派模型进行类的加载,这种模型确保了Java核心API的稳定性和防止了一些安全问题。
测试:获得类加载器
这段Java代码定义了一个名为Test04
的公共类,其中包含了main
方法作为程序的入口点。
(1)如何获取Java中的类加载器及其父类加载器
该程序主要展示了如何获取并打印Java中的类加载器及其父类加载器。
public class Test04 {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
}
}
具体来说,代码做了以下几件事情:
- 通过调用
ClassLoader.getSystemClassLoader()
方法获取了系统类加载器(System ClassLoader),它通常用于加载应用程序classpath下的类。 - 打印了系统类加载器的信息。这通常会输出类加载器的类名和一些详细信息,比如类加载器的哈希码等。
- 调用系统类加载器的
getParent()
方法获取其父类加载器,即扩展类加载器(Extension ClassLoader)。这个类加载器用于加载Java的扩展类库。 - 打印了扩展类加载器的信息。
- 再次调用父类加载器的
getParent()
方法,理论上应该获取到启动类加载器(Bootstrap ClassLoader)。但是,启动类加载器在Java中是用C++实现的,因此在Java代码中并不能直接获取到它的实例。因此,这段代码会打印出null
。
(2)测试当前类是哪个类加载器
public class Test04 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
ClassLoader classLoader = Class.forName("com.itheima.sjms.Test04").getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader1);
}
}
在这段扩展后的Java代码中,除了原有的类加载器层次结构打印之外,还添加了两行用于通过Class.forName()
方法获取特定类的类加载器,并打印它们的信息。
-
使用
Class.forName("com.itheima.sjms.Test04").getClassLoader():
获取Test04
类的类加载器并打印。-
这将返回加载
Test04
类的类加载器,如果Test04
类位于应用程序的classpath下,那么这通常是系统类加载器。
-
-
使用
Class.forName("java.lang.Object").getClassLoader():
获取java.lang.Object
类的类加载器并打印。-
这将返回加载
java.lang.Object
类的类加载器。由于Object
类是Java核心类库的一部分,它通常是由引导类加载器加载的。但是,在这里打印出的结果可能会是null
,因为如前所述,引导类加载器在Java中是不可见的,无法直接获取。
-
(3)获取系统类加载器可以加载的路径
在Java中,System.getProperty("java.class.path")
是用来检索Java类路径(classpath)的系统属性。类路径是JVM和Java工具用来查找用户定义的类和第三方库的一组目录和文件。
public class Test04 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
ClassLoader classLoader = Class.forName("com.itheima.sjms.Test04").getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader1);
System.out.println(System.getProperty("java.class.path"));
}
}
这行代码会打印出当前Java进程的类路径设置。类路径通常包含:
- 当前目录(
.
) - JAR文件的路径(以分号
;
分隔,在Windows系统中;在Unix/Linux系统中使用冒号:
分隔) - 包含
.class
文件的目录 - 以及其他可能包含类或资源的目录和文件
D:\my\else\code-learning\java-study\code\out\production\exercises;
D:\my\else\code-learning\java-study\code\exercises\lib\gson-2.6.2.jar;
D:\my\else\code-learning\java-study\code\tankwar2.zip;
D:\my\else\code-learning\java-study\code\1_bsm-30min.jar;C:\Users\xxx\.m2\repository\org\junit\jupiter\junit-jupiter\5.8.1\junit-jupiter-5.8.1.jar;
C:\Users\xxx\.m2\repository\org\junit\jupiter\junit-jupiter-api\5.8.1\junit-jupiter-api-5.8.1.jar;
C:\Users\xxx\.m2\repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;
C:\Users\xxx\.m2\repository\org\junit\platform\junit-platform-commons\1.8.1\junit-platform-commons-1.8.1.jar;
C:\Users\xxx\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;
C:\Users\xxx\.m2\repository\org\junit\jupiter\junit-jupiter-params\5.8.1\junit-jupiter-params-5.8.1.jar;
C:\Users\xxx\.m2\repository\org\junit\jupiter\junit-jupiter-engine\5.8.1\junit-jupiter-engine-5.8.1.jar;
C:\Users\xxx\.m2\repository\org\junit\platform\junit-platform-engine\1.8.1\junit-platform-engine-1.8.1.jar
类路径是在启动Java应用程序时通过-cp
或-classpath
命令行选项设置的,或者由环境变量CLASSPATH
指定(尽管环境变量方法不太推荐,因为它可能对系统中的所有Java应用程序产生全局影响)。
类路径对于Java应用程序至关重要,因为JVM需要知道在哪里可以找到所有的类和资源文件,以便能够正确地加载和运行程序。如果类路径设置不正确,JVM可能会抛出ClassNotFoundException
或其他相关异常,因为它找不到必要的类文件。
双亲委派机制
双亲委派机制是Java中的一种类加载机制。当一个类加载器收到类加载请求时,它不会自己首先去加载这个类,而是将这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
这种机制的设计主要是为了确保Java核心API的稳定性和防止代码被一些恶意的代码所篡改。例如,如果没有使用双亲委派模型,而是由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
自己如果写一个自己的String类,路径是java.lang.String,可以吗?
在Java中,自己写一个类并将其放在java.lang
包下,尤其是命名为String
,是不被推荐的,实际上也是不允许的,原因主要有以下几点:
-
包名保护:
java.lang
是Java核心API的包名,它是保留给Java平台使用的。试图在自己的代码中使用这个包名可能会导致编译时错误或警告,甚至在某些环境中可能根本无法编译。 -
类加载器的双亲委派机制:即使你能够编译一个类并将其放入
java.lang
包,Java的类加载器也不会加载它。-
双亲委派机制确保核心类库中的类总是被优先加载,而且是由最顶层的类加载器(即引导类加载器)加载的。自定义的
java.lang.String
类不会由系统类加载器或引导类加载器加载,因为引导类加载器已经加载了Java平台提供的String
类。
-
-
安全性:Java的安全模型不允许覆盖或修改核心类库中的类,以防止恶意代码破坏Java运行环境的安全性和稳定性。
-
二进制兼容性:Java的核心类库是为所有Java程序提供的通用接口,它们的二进制兼容性非常重要。允许自定义这些类可能会引入不一致性,从而破坏不同Java应用程序之间的互操作性。
-
类和方法的
final
修饰符:在java.lang
包中的很多类和方法都被声明为final
,包括String
类。final
类不能被继承,这意味着你不能创建一个新的String
子类,更不用说替换原始的String
类了。 -
已有的
String
类:Java中已经有了一个功能完善、经过严格测试和优化过的String
类。在大多数情况下,你不需要(也不应该)重新实现这个功能。如果你需要不同的字符串处理功能,可以通过扩展现有的类或使用组合来实现,而不是尝试从头开始创建一个新的String
类。