类的初始化步骤
- 假如这个类还没有被加载和连接,那就先进行加载和连接
- 假如这个类存在直接父类,而且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化语句,那就依次执行这些初始化语句
类的初始化时机
主动使用(七种)
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName("com.test.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(Test类包含main()方法,使用Java Test启动)
- JDK1.7开始提供的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化两种途径
- 在静态变量的声明处进行初始化
- 在静态代码块中进行初始化
public class Sample {
private static int a = 1;
private static long b;
private static long c;
static {
b = 2;
}
}
例如在上述代码中,静态变量a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值0,再如下面这个例子
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");
}
}
/*
对于静态字段来说,只有直接定义了该字段的类才会被初始化
当一个类在初始化时,要求其父类全部都已经初始化完毕了
-XX:+TraceClassLoading 用于追踪类的加载信息并打印出来
-XX:+TraceClassUnloading 用于追踪类的卸载信息并打印出来
-XX:+<option> 表示开启option选项
-XX:-<option> 表示关闭option选项
-xx:<option>=<value> 表示将option选项的值设置为value
*/
可以运行下这个程序,看看输出结果,会出现这个结果的原因是,对于静态字段来说,只有直接定义了该字段的类才会被初始化
静态变量的声明语句,以及静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件的先后顺序依次执行它们,例如
public class Sample {
private static int a = 1;
static {
a = 2;
}
static {
a = 4;
}
public static void main(String[] args) {
System.out.println("a = " + a);
}
}
当上述代码的Sample类被初始化后,它的静态变量a的取值为4,再如下面这个代码,有两个Singleton类是注释的,可以打开注释运行下程序,看看结果
/*
答案 2,0
分析如下
Singleton singleton = Singleton.getInstance();
调用了类的静态方法,会对类进行初始化,分准备阶段和初始化阶段
准备阶段
counter1 = 0
singleton = null
私有构造方法不执行
counter2 = 0
初始化阶段,按照代码顺序进行初始化
counter1 = 1
singleton = new Singleton()会执行私有构造器,counter1 = 2,counter2 = 1
counter2 = 0
所以,答案是2,0
*/
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("Singleton.counter1 = " + Singleton.counter1);
System.out.println("Singleton.counter2 = " + Singleton.counter2);
}
}
class Singleton {
public static int counter1 = 1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
/*
class Singleton {
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
*/
/*
class Singleton {
public static int counter1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
*/
对于常量来说,当一个常量的值在编译期间可以确定,那么常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。具体看下面两个例子
/*
常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用
到定义常量的类,因此不会触发定义常量的类的初始化
注意:这里指的是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了,
甚至,我们可以将MyParent2的class文件删除
*/
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
System.out.println(MyParent2.s1);
System.out.println(MyParent2.s2);
System.out.println(MyParent2.i1);
System.out.println(MyParent2.i2);
}
}
class MyParent2 {
public static final String str = "Hello World";
public static final short s1 = 7;
public static final short s2 = 128;
public static final int i1 = 0;
public static final int i2 = -2;
static {
System.out.println("MyParent2 static block");
}
}
import java.util.UUID;
/*
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动
使用这个常量所在的类,显然会导致这个类被初始化
*/
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString().replace("-", "");
static {
System.out.println("MyParent3 static block");
}
}
对于数组来说,对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为[Lcom.zj.study.jvm.classloader.MyParent4这种形式。动态生成的类型,其父类型就是Object
JavaDoc经常将构成数组的元素称为Component,实际上就是将数组降低一个维度后的类型,看代码
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[10];
// [Lcom.zj.study.jvm.classloader.MyParent4 表示一个具体的类型,是java虚拟机在运行期生成出来的,显然是一个数组类型,有点类似动态代理
System.out.println(myParent4s.getClass());
System.out.println(myParent4s.getClass().getSuperclass());
System.out.println("=================");
MyParent4[][] myParent4ss = new MyParent4[10][10];
// [Lcom.zj.study.jvm.classloader.MyParent4 表示一个具体的类型,是java虚拟机在运行期生成出来的,显然是一个数组类型,有点类似动态代理
System.out.println(myParent4ss.getClass());
System.out.println(myParent4ss.getClass().getSuperclass());
System.out.println("=================");
int[] intNums = new int[100];
System.out.println(intNums.getClass());
System.out.println(intNums.getClass().getSuperclass());
System.out.println("=================");
Integer[] intNumss = new Integer[100];
System.out.println(intNumss.getClass());
System.out.println(intNumss.getClass().getSuperclass());
System.out.println("=================");
}
}
class MyParent4{
static {
System.out.println("MyParent4 static block");
}
}
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化
使用子类去访问父类的静态变量/静态方法,本质上表示对父类的主动使用,而不是对子类的主动使用,定义在谁身上,表示对谁的主动使用
看下代码
public class MyTest9 {
static {
System.out.println("MyTest9 static block");
}
public static void main(String[] args) {
System.out.println(MyChild9.b);
}
}
class MyParent9{
static int a = 3;
static {
System.out.println("MyParent9 static block");
}
}
class MyChild9 extends MyParent9 {
static int b = 4;
static {
System.out.println("MyChild9 static block");
}
}
分析下程序的运行结果,再看下一个代码
public class MyTest10 {
static {
System.out.println("MyTest10 static block");
}
/*
MyTest10 static block
--------
MyParent10 static block
--------
3
--------
MyChild10 static block
4
*/
public static void main(String[] args) {
MyParent10 myParent10;
System.out.println("--------");
myParent10 = new MyParent10();
System.out.println("--------");
System.out.println(myParent10.a);
System.out.println("--------");
System.out.println(MyChild10.b);
}
}
class MyParent10{
static int a = 3;
static {
System.out.println("MyParent10 static block");
}
}
class MyChild10 extends MyParent10 {
static int b = 4;
static {
System.out.println("MyChild10 static block");
}
}
分析下运行的结果,在看一个例子
// 使用子类去访问父类的静态变量/静态方法,本质上表示对父类的主动使用,而不是对子类的主动使用
// 定义在谁身上,表示对谁的主动使用
public class MyTest11 {
/*
MyParent11 static block
3
do something
*/
public static void main(String[] args) {
System.out.println(MyChild11.a);
MyChild11.doSomething();
}
}
class MyParent11 {
static int a = 3;
static {
System.out.println("MyParent11 static block");
}
static void doSomething() {
System.out.println("do something");
}
}
class MyChild11 extends MyParent11 {
static {
System.out.println("MyChild11 static block");
}
}
分析运行的结果,这是因为,使用子类去访问父类的静态变量/静态方法,本质上表示对父类的主动使用,而不是对子类的主动使用,定义在谁身上,表示对谁的主动使用
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用,看例子
看例子之前,先明白实例化代码块与静态代码块的区别,
class C {
// 实例化代码块,每一个C的实例被创建时都会执行
{
System.out.println("Hello");
}
// 静态代码块,仅仅会执行一次
static {
System.out.println("Static ");
}
public C() {
System.out.println("C");
}
}
为了验证接口有没有没初始化,定义了如下类似的接口
interface MyParent5 {
// 如果MyParent5被初始化了,那么thread一定会被赋值,那么必会创建一个匿名对象
// 代码块会被执行 MyParent5 invoked会被执行
public static final Thread thread = new Thread() {
{
System.out.println("MyParent5 invoked");
}
};
}
首先看下,在初始化一个类时,并不会先初始化它所实现的接口,上代码
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
System.out.println(MyChild5.thread);
}
}
// 在初始化一个类时,并不会先初始化它所实现的接口
interface MyParent5 {
// 如果MyParent5被初始化了,那么thread一定会被赋值,那么必会创建一个匿名对象
// 代码块会被执行
public static final Thread thread = new Thread() {
{
System.out.println("MyParent5 invoked");
}
};
}
class MyChild5 implements MyParent5 {
public static int b = 5;
}
分析以上代码,看运行结果,当一个接口在初始化时,并不要求其父接口都完成了初始化,只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化,接口定义的变量,本身就是public static final修饰的,本身就是常量
再来看下,在初始化一个接口时,并不会先初始化它的父接口
public class MyTest5_1 {
public static void main(String[] args) {
System.out.println(NewMyParent5.thread);
}
}
// 在初始化一个接口时,并不会先初始化它的父接口
interface NewMyParent5 extends NewMyGradPa5 {
public static Thread thread = new Thread() {
{
System.out.println("NewMyParent5 invoked");
}
};
}
interface NewMyGradPa5 {
public static Thread thread = new Thread() {
{
System.out.println("NewMyGradPa5 invoked");
}
};
}
分析以上代码,看运行结果