继上一篇博客【注释和反射】类加载的过程-CSDN博客
目录
四、什么时候类会被初始化(主动引用)?
在Java中,类的初始化是类加载过程的一部分,具体地说,它发生在类加载的"初始化"阶段。但是,类的初始化并不会在类被加载到JVM时立即发生,而是在以下情况之一发生时才会被触发:
-
创建类的实例 :当使用
new
关键字创建类的新实例时,如果该类还没有被初始化,则会触发类的初始化。 -
访问某个静态变量 :当类中的静态变量被访问时(除非该静态变量是常量,即被
final
修饰的基本类型或字符串常量),如果该类尚未初始化,则会触发初始化。需要注意的是,只有真正读取静态变量的值才会触发初始化,而仅仅声明静态变量不会触发初始化。 -
调用类的静态方法:当调用类的静态方法时,如果该类还没有被初始化,则会先进行初始化。
-
反射 :当使用Java反射API(如
Class.forName()
方法)显式加载类时,会触发类的初始化。这是框架和库常用的技术,用于动态加载和初始化类。 -
初始化子类 :当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
Java虚拟机启动时 :当Java虚拟机启动时,会先初始化包含
main
方法的那个类。 -
被标记为
@interface
的接口 :在Java中,接口在加载后、初始化前,会为接口中定义的常量(只能是public static final
)进行赋值。然而,接口本身并不需要进行传统意义上的"初始化",因为它们不能有实例字段或实例初始化块。
需要注意的是,
- 静态代码块(
static {}
)会在类的初始化过程中执行,且只执行一次。 - 静态代码块的执行顺序是按照它们在类中出现的顺序。
- 此外,静态变量的初始化也是类初始化的一部分,静态变量会在任何静态方法(包括静态代码块)被调用之前初始化,除非该变量是常量(
final
修饰的静态变量)。
了解这些触发条件有助于更好地理解和控制Java程序的类加载和初始化行为。
测试
首先,创建一个父类和继承它的子类。
java
class F{
static int b = 2;
static {
System.out.println("父类被加载");
}
}
class S extends F{
static {
System.out.println("子类被加载");
m = 300;
}
static int m = 100;
static final int M = 1;
}
(1) 当使用new
关键字创建类的新实例时,如果该类还没有被初始化,则会触发类的初始化。
java
public class Test03 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) {
S s = new S();
}
}
(2)反射也会触发类的初始化
java
public class Test03 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.itheima.sjms.S");
}
}
五、什么情况下不会发生类的初始化(被动引用)?
在Java中,有几种情况下类的引用并不会触发类的初始化,这些情况通常被称为"被动引用"。以下是被动引用的几种情况:
-
通过子类引用父类的静态字段:如果仅通过子类来引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化。这是因为静态字段属于类本身,而不属于类的任何实例。
-
通过数组定义来引用类 :当定义一个类的数组,但并未实际创建该类的实例或访问其静态字段/方法时,不会触发类的初始化。例如,
MyClass[] array;
这行代码本身不会导致MyClass
的初始化。 -
常量在编译阶段的引用 :如果一个类的静态常量(被
final
修饰的静态变量,且其值在编译时已知)被另一个类引用,那么在编译阶段,这个常量的值会被直接嵌入到引用它的类中,因此不会触发定义常量的类的初始化 。这是因为编译器在编译时就已经将常量的值替换到了引用它的代码中,这个过程称为常量折叠。 -
仅加载类而不初始化 :在某些情况下,Java虚拟机可能仅加载类而不初始化它。例如,当使用
ClassLoader
类的loadClass
方法来加载类时,可以选择是否进行类的初始化。如果传递的参数为false
,则仅加载类而不进行初始化。 -
接口类型的引用:接口的引用通常不会触发接口的初始化。接口本身不包含实例字段,因此它们不需要像类那样进行初始化。只有当接口中定义的常量被实际使用时,接口才会被加载和初始化。
了解这些情况有助于优化Java程序的性能,因为可以避免不必要的类加载和初始化开销。同时,这也有助于理解Java类加载器和类加载机制的内部工作原理。
测试
首先,创建一个父类和继承它的子类。
java
class F{
static int b = 2;
static {
System.out.println("父类被加载");
}
}
class S extends F{
static {
System.out.println("子类被加载");
m = 300;
}
static int m = 100;
static final int M = 1;
}
(1)通过子类调用父类的一个静态变量,
java
public class Test03 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) {
System.out.println(S.b);
}
}
此时不会对子类产生任何影响,子类都不会被加载。
(3)创建一个子类的数组,
java
public class Test03 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) {
S[] arr = new S[5];
}
}
我们可以看到,只有main类被加载了,因为虚拟机启动的时候,先会初始化main方法所在的类。初始化完之后,S[] arr = new S[5];执行这行代码,没有任何类被加载,因为这个数组只是一个名字和一片空间而已,并不会加载类。
(3)调用子类的常量,
java
public class Test03 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) {
System.out.println(S.M);
}
}
可以看到代码的执行结果中,子类和父类都没有被加载,因为所有的常量和类的静态变量都是在链接阶段就已经赋值了,所以初始化的时候已经存在了。