我们知道,《Java虚拟机规范》章节5.5 Initialization ^(1)^ 中严格规定了有且只有六种情况必须立即对类进行初始化:

  • 遇到new(实例化对象)、getstatic(读取一个没有被final修饰、没有在编译期把结果放入常量池的类的静态字段)、putstatic(设置一个没有被final修饰、没有在编译期把结果放入常量池的类的静态字段)或者invokestatic(调用一个类的静态方法)这四条字节码指令时,如果类没有进行初始化则需要触发初始化。
  • 使用java.lang.reflect包的方法对类型进行反射调用
  • 父类还没有初始化
  • 虚拟机启动时,用户需要制定一个要执行的主类(main)
  • 当使用java7新加入的动态语言支持时,如果一个MthodHandle实例最后的解析结果是REF_getStatic、REF_putstatic、REF_invokestatic、REF_newInvokeSpecial四种类型的方法句柄
  • 一个接口定义了default修饰的接口方法,同时接口的实现类发生了初始化

除此之外,所有引用类型的方式都不会触发初始化,称为被动引用:

子类引用父类

通过子类引用父类的静态字段不会导致子类初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo;

class Super{
static {System.out.println("Super");}
public static int value = 1;
}
class Sub extends Super{
static{System.out.println("Sub");}
}

public class SomeName{
public static void main(String[] args){
System.out.println(Sub.value);
}
}

结果是什么呢?会先输出“Super”,然后是value的值。

对于静态字段,只有直接定义这个字段的类才会被初始化,所以通过子类引用父类静态字段,也只有父类会初始化。

数组引用类

通过数组定义来引用类,不会触发此类的初始化

1
2
3
4
5
6
public class SomeName{
public static void main(String[] args){
Sub[] sp = new Sub[5];
}
}

结果是什么呢?

什么也没有。

可见Sub类并没有被初始化。不过这段代码触发了另一个名为“[Lcom.example.demo.Sub”的类的初始化阶段。这玩意是虚拟机自动生成的直接继承自Object的子类,创建动作由字节码指令anewarray(即新建引用数组)触发。

这个类代表的一维数组,用户可直接使用的只有被修饰为Public的length属性和clone方法,当然数组中应用的属性和方法都实现在这个类中。这是因为Java包装了数组元素的访问(在C中是数组指针的移动),这也就是为什么Java检测到数组越界会抛出ArrayIndexOutOfBoundsException异常而不是像C中的非法内存访问。

准确的说,Java的越界检查不是封装在数组元素访问的类中,而是封装在数组访问的xaload(数组的元素压栈)、xastore(针对数组的操作)字节码指令。

常量池引用

常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类

1
2
3
4
5
6
7
8
9
10
11
class Super{
static {System.out.println("Super");}
public static final String HELLO = "Hello";
}


public class SomeName{
public static void main(String[] args){
System.out.println(Super.HELLO);
}
}

这里的代码运行后也不会输出“Super”。

hello的常量值其实在编译阶段已经被转化成SomeName类对自身常量池的引用了,所以他们俩没什么关系了。

接口

接口和类有一点不同。接口其实也有初始化过程,不过接口不能像类一样用static代码块来输出初始化信息,编译器会为接口生成“”类构造器,用于初始化接口中定义的成员变量。并且:

接口不要求父接口全部初始化,只有用到了才会初始化。


引用

(1) Yellin, F. and Lindholm, T., 1996. The java virtual machine specification.