一个Java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况。如图所示:
类的初始化做了什么
- 为类的静态变量赋予正确的初始值。
- 执行类的静态代码块。
按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
什么时候类会被初始化
- 只有类或接口被Java程序首次主动使用时才初始化他们。
主动使用(7种)
- 创建类的实例(new)。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 通过反射方式执行以上三种行为。
- 初始化子类的时候,会触发父类的初始化。
- Java虚拟机启动时被标明为启动类的类。(有main方法的类)
- JDK 1.7开始提供的动态语言支持。(了解即可)
除了以上7种情况,其它使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
类的静态字段和类初始化
根据主动使用导致类的初始化规则,访问类的静态变量会导致类的初始化。
/**
* 对于静态字段来说,只有直接定义了该字段的类才会被初始化,
* 当一个类初始化时,要求其父类全部都已经初始化完毕了。
*
* -XX:+TraceClassLoading,用于追踪类的加载信息并打印。
* -XX:+TraceClassUnLoading,用于追踪类的卸载信息并打印。
*/
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
class MyParent1 {
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1 {
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
打印的结果是:
MyParent1 static block
MyChild1 static block
welcome
访问MyChild1类的静态字段str2,使得类MyChild1被初始化,子类的初始化触发父类的初始化,所以MyParent1类初始化。
如果访问MyChild1.str
,那么打印的结果变成:
MyParent1 static block
hello world
因为对于静态字段来说,只有直接定义了该字段的类才会被初始化。
类的编译期常量和类初始化
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2 {
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
打印结果:
hello world
这里MyParent2的静态代码块没有执行,说明MyParent2没有被初始化!其实MyParent2类在这里没有被主动使用,所以没初始化。因为静态字段str 是常量。常量在编译阶段会存入到调用这个常量的方法所在类的常量池中。本质上,调用类并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化,也不会加载这个类。
类的运行期常量和类初始化
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = UUID
.randomUUID()
.toString();
static {
System.out.println("MyParent3 static block");
}
}
打印结果:
MyParent3 static block
4e5bc60b-ec26-40c1-aeea-a5eddcb2dbaf
静态代码块执行,说明类MyParent3 被初始化。这个例子与上个例子不一样的是这里的常量不是编译阶段就可以确定的,它要运行期间才确定。
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类会初始化。
接口初始化规则
- 接口中的字段默认都是public static final。
- 接口中不能定义静态代码块。
- 当一个接口初始化时,并不要求其父接口都完成了初始化只有在真正使用到父接口的时候(如引用到接口中所定义的常量时),才会初始化(但子接口初始化,父接口一定会被加载)。
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5 {
int a = new Random().nextInt(4);
Thread thread1 = new Thread() {
{
System.out.println("MyParent5初始化了");
}
};
}
interface MyChild5 extends MyParent5 {
int b = 4;
Thread thread2 = new Thread() {
{
System.out.println("MyChild5初始化了");
}
};
}
打印结果:
4
因为接口里不能定义静态代码块,所以为了能看出接口是否被初始化,这里我在接口里定义个Thread类型的变量,并返回个对象引用。如果接口初始化了,它的Thread类型的变量一定会被实例化,就会执行定义的构造代码块里的代码。(静态代码块,局部代码块,构造代码块区别。)
这里没有打印构造代码块里的内容,说明接口没有被初始化。这是因为接口中的字段默认都是public static final。访问接口的(静态常量)字段不会导致接口初始化,这里跟访问类的静态常量字段一样的,所以上面的结论适用于接口。
子接口初始化不会导致父接口初始化
把上面的例子做个改动,如下。
interface MyChild5 extends MyParent5 {
int b = new Random().nextInt(4);
Thread thread2 = new Thread() {
{
System.out.println("MyChild5初始化了");
}
};
// public static int b = 4;
}
打印结果:
MyChild5初始化了
0
这里的常量是运行期间才能确定,所以MyChild5接口被初始化。但是从结果明显看出,仅仅初始化了子接口并没有初始化父接口!
但是类的初始化是:初始化子类的时候,会触发父类的初始化。这就是类的初始化和接口初始化的区别了。
直接访问父接口定义的常量
继续改进代码,如下:
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.a);
}
}
打印结果:
MyParent5初始化了
0
直接访问父接口的静态常量字段a(运行期间确定),导致父接口初始化。
实现类初始化不会导致接口初始化
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5 {
int a = new Random().nextInt(4);
Thread thread1 = new Thread() {
{
System.out.println("MyParent5初始化了");
}
};
}
class MyChild5 implements MyParent5 {
public static Thread thread2 = new Thread() {
{
System.out.println("MyChild5初始化了");
}
};
public static int b = 4;
}
打印结果:
MyChild5初始化了
4
这个例子说明,子类被初始化了,但是它实现的接口没有初始化。
总结
类只有被首次主动使用时才会被初始化,主动使用有7种形式。这里我并没有对每一种主动使用都去举例论证。这几个例子是我在学习《深入理解JVM》这门课程时感觉最难理解的,也是最重要的知识点。
- 编译期常量和运行期常量对类初始化的影响。
- 初始化对于类与接口的异同点。
其它几种对类的主动使用,大家可以自己去通过代码去验证文章的结论,这样能加深自己的理解。