类加载时的初始化顺序

基础知识

一个Java文件从类层面上可以分为两部分,静态和非静态。静态部分属于类所有,非静态部分为对象所有,静态部分为所有对象共享。

类的加载

当我们使用一个类的时候,我们必须先初始化这个类,它由Java虚拟机完成,那么具体的初始化顺序是怎样的呢?
Java虚拟机先初始化类的静态部分(包括静态变量、静态常量、静态代码块),然后初始化非静态部分(包括成员变量、常量、非静态代码块),构造函数最后执行。

静态成员变量和成员变量的初始化

public class TestB {
    private int i = print("非静态");
    private static int j = print("静态");
    public static int print(String s) {
        System.out.println(s);
        return 0;
    }
    public static void main(String[] args) {
    }
}
//output:
静态

可以看到静态变量执行初始化了,而成员变量没有,因为成员变量只有在创建对象的时候才会初始化,如下:

public class TestB {
    private int i = print("非静态");
    private static int j = print("静态");
    public static int print(String s) {
        System.out.println(s);
        return 0;
    }
    public static void main(String[] args) {
        new TestB();
    }
}
//output:
静态
非静态

静态变量初始化优先于成员变量。其余也是,有兴趣可以验证。

静态部分只会初始化一次

public class TestB {
    private int i = print("非静态");
    private static int j = print("静态");
    public static int print(String s) {
        System.out.println(s);
        return 0;
    }
    public static void main(String[] args) {
        new TestB();
        new TestB();
    }
}
//output:
静态
非静态
非静态

可以看到,每创建一个对象成员变量就会执行初始化一次,而静态变量只会初始化一次。

静态部分的初始化顺序

先按书写顺序从上到下声明静态变量并赋予初值(注意,并不是你给它赋的值)。
声明变量这个过程并不会初始化这个变量声明的类
比如private A a;声明这个变量并不会初始化A这个类

类型 初始值
boolean false
byte (byte)0
char ‘u0000’(null)
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
reference null

然后再按书写顺序,从上到下执行静态代码块和静态变量赋值,(注意是按照书写顺序,哪个在上先执行哪个)。

public class TestB {
    static {
        print("静态代码块1");
    }
    private static int i = print("静态变量1");
    static {
        print("静态代码块2");
    }
    public static int print(String s) {
        System.out.println("s=" + s + "      i=" + i);
        return 1;
    }
    public static void main(String[] args) {
    }
}
//output:
s=静态代码块1      i=0
s=静态变量1      i=0
s=静态代码块2      i=1

仔细体会它的执行过程。如果是静态常量(static final)那么一定要在静态部分初始化完成之前,完成对他的赋值。

非静态部分的初始化顺序

非静态部分的初始化顺序和静态的相同,有兴趣可以自行验证。

new关键字

类什么时候开始初始化?其实是从这个类的静态部分引用开始的,比如说

class A{
	static Object o = new Object();
}
class B{
	static Object o = A.o;
}

B类初始化时引用了A类的静态属性o,所以A类必须初始化。但是为什么我们new一个对象时也会执行初始化呢?因为其实构造函数也是一个静态引用,只不过它没有字面上的static关键字。(注意,有一种情况特殊,就是编译期静态常量,对它的引用不会触发初始化操作)。

public class TestB {
    private static int i = V.i;
    public static void main(String[] args) {
    }
}
class V{
    static final int i = 1;
    static{
        System.out.println("V类的静态代码块");
    }
}
//output:

没有打印输出,说明结论成立。

编译期静态常量:这类常量必须是基本数据类型和字符串字面量,在编译阶段通过常量传播优化的方式将类的常量存储到了一个类的常量池中,在以后对类常量的引用实际都转化为类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在类的常量池获取,这也就是引用编译期静态常量不会触发类初始化的重要原因

构造函数是创建对象的初始化的最后阶段,常量必须在构造函数完成之前完成,否则将报编译器错误。

试一试

下面是一道面试题,大家可以试试水:

public class Test {
    public static int k = 0;
    public static Test t1 = new Test("t1");
    public static Test t2 = new Test("t2");
    public static int i = print("i");
    public static int n = 99;
    private int a = 0;
    public int j = print("j");
    {
        print("构造块");
    }
    static {
        print("静态块");
    }
    public Test(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++i;
        ++n;
    }
    public static int print(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++n;
        return ++i;
    }
    public static void main(String args[]) {
        Test t = new Test("init");
    }
}

执行结果:

1:j    i=0     n=0
2:构造块    i=1     n=1
3:t1    i=2     n=2
4:j    i=3     n=3
5:构造块    i=4     n=4
6:t2    i=5     n=5
7:i    i=6     n=6
8:静态块    i=7     n=99
9:j    i=8     n=100
10:构造块    i=9     n=101
11:init    i=10     n=102

先声明静态变量赋予初值,然后执行真正的赋值操作,但是当执行到第二句“public static Test t1 = new Test(“t1”);”发现要去执行初始化对象,可此时静态变量还没有初始化完成,怎么办?它会先去执行对象的初始化,即声明成员变量赋予初值,然后真正赋值(变量赋值和非静态代码块),再去执行构造函数。完成之后再继续执行未完成的静态变量初始化。(注意,静态部分只会初始化一次)

带有继承关系的初始化

如果不需要实例化子类对象,那么先初始化父类的静态部分按声明的顺序,全部完成之后再初始化子类的静态部分按声明顺序。
如果父类还有父类,一样先初始化最顶层的类,再一级级初始化下来。

public class TestC extends Sup{
    static {
        print("子类的静态代码块");
    }
    static int i = print("子类的静态成员初始化");
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    public static void main(String[] args) {
    }
}
class Sup {
    static int i = print("父类的静态成员初始化");
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    static {
        print("父类的静态代码块");
    }
}
//output:
父类的静态成员初始化
父类的静态代码块
子类的静态代码块
子类的静态成员初始化

如果需要实例化子类对象,就要在之前的基础上再加上初始化非静态部分。先执行父类的非静态部分的初始化和构造函数,再执行子类的。

public class TestC extends Sup{
    static {
        print("子类的静态代码块");
    }
    static int i = print("子类的静态成员初始化");
    int j = print("子类的成员初始化");
    {
        print("子类的非静态代码块");
    }
    public TestC(){
        print("子类的构造函数");
    }
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    public static void main(String[] args) {
        new TestC();
    }
}

class Sup {
    static int i = print("父类的静态成员初始化");
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    static {
        print("父类的静态代码块");
    }
    int j = print("父类的成员初始化");
    {
        print("父类的非静态代码块");
    }
    public Sup(){
        print("父类的构造函数");
    }
}
//output:
父类的静态成员初始化
父类的静态代码块
子类的静态代码块
子类的静态成员初始化
父类的成员初始化
父类的非静态代码块
父类的构造函数
子类的成员初始化
子类的非静态代码块
子类的构造函数

静态初始化——>父类成员声明->子类成员声明->父类成员赋值初始化(父类非静态代码块)->父类构造函数->子类成员赋值初始化(子类非静态代码块)->子类构造函数

子类重写父类的方法并在初始化的时候执行

public class TestC extends Sup{
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    int k() {
        return print("子类的方法");
    }
    public static void main(String[] args) {
        new TestC();
    }
}

class Sup {
    static int print(String s) {
        System.out.println(s);
        return 1;
    }
    int h = k();
    int k(){
       return print("父类的方法");
    }
}
//output:
子类的方法

从输出可以知道,初始化子类,当子类重写了父类的方法时,父类在初始化时就执行子类重写的方法。

目前想到的就这么多了,如果还有,以后会补充。

猜你喜欢

转载自blog.csdn.net/qq_36744284/article/details/89332947