【细说Java】Java变量初始化顺序

时间:2023-12-13 12:52:26

  Java的变量初始化顺序,对这里一直似懂非懂,面试的时候也经常被问到,但答的一直不好,现在整理记录一下,以后忘记了可以来看看。

  程序分为两个部分,第一个部分不考虑继承,第二个部分考虑继承;

(1)不考虑继承的情况

代码如下:

public class JavaTest {
    public JavaTest() {
        System.out.println("执行JavaTest构造方法1");
    }

    public JavaTest(String param) {
        System.out.println("执行JavaTest构造方法2");
    }

    static {
        System.out.println("JavaTest静态代码块1");
    }

    {
        System.out.println("JavaTest代码块1");
    }

    private static int max1 = getMax1();
    private int min1 = getMin1();

    public int getMin1() {
        System.out.println("初始化成员变量min1");
        return 0;
    }

    public static int getMax1() {
        System.out.println("初始化静态成员变量max1");
        return 0;
    }

    static {
        System.out.println("JavaTest静态代码块2");
    }

    {
        System.out.println("JavaTest代码块2");
    }

    private static int max2 = getMax2();
    private int min2 = getMin2();

    public int getMin2() {
        System.out.println("初始化成员变量min2");
        return 0;
    }

    public static int getMax2() {
        System.out.println("初始化静态成员变量max2");
        return 0;
    }

    public static void main(String[] args) {
        System.out.println("==============================");
        new JavaTest();
        System.out.println("==============================");
        new JavaTest("param");
    }
}

简单说一下:本实例中,共声明了两个静态代码块,两个初始化块,两个构造器,两个静态成员变量,两个非静态成员变量,并分散的声明开了,这个程序仅仅是为了做测试,来证明各种初始化方式的顺序,实际编写时切勿这样写,否则会使程序非常难于维护。

执行结果:

JavaTest静态代码块1
初始化静态成员变量max1
JavaTest静态代码块2
初始化静态成员变量max2
==============================
JavaTest代码块1
初始化成员变量min1
JavaTest代码块2
初始化成员变量min2
执行JavaTest构造方法1
==============================
JavaTest代码块1
初始化成员变量min1
JavaTest代码块2
初始化成员变量min2
执行JavaTest构造方法2

通过以上结果我们可以得出一些结论结论:

静态的初始化要先于实例的初始化,并且只执行一次。静态的初始化后,才开始为实例变量分配空间,执行初始化,最后执行构造器。

具体执行情况可以总结如下:

(1)在类加载时,为类中的静态成员变量分配内存空间,并初始化默认值;

(2)执行静态成员变量的初始化操作。而静态成员的初始化有两种方式:在声明时直接初始化与静态代码块。两种初始化方式会按照在类中出现的顺序(声明的顺序)来执行。

(3)上面两步只会在类加载时执行一次;

(4)如果创建了类的对象,便在堆中为类的实例分配内存空间,并初始化默认值;

(5)执行实例变量的初始化操作。同样有两种方式:声明时直接初始化与初始化块。这两种方式也是按照在类中出现的顺序来执行;

(6)执行类的构造器方法。

  注意:虽然类的成员变量可以在声明时为其变量直接初始化,但声明与初始化并不是同时执行的。对于静态成员变量,会先为所有类中声明的静态成员变量分配空间,每个变量存在默认值后,才会执行变量声明处的初始化,而这种初始化方式是静态初始化块按照在类中出现的先后顺序来执行的。对于实例变量,与静态变量是类似的。

  既然类的变量空间分配是先于初始化执行的,那么就存在这样一种情况,在变量创建之后,而在变量的初始化之前。如果在此期间使用此变量,就可能得不到我们想要的结果。

用一个小例子说明一下:

public class Test2 {
    {
        print();
    }

    private int max = 9;
    private String maxValue;

    public String getMax() {
        return maxValue;
    }

    public void print() {
        maxValue = "max: " + max;
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        System.out.println(test2.getMax());
    }
}

大家看一下打印结果就知道了:

max: 0

  正常情况下,应该能打印出9的,但却打印出了0。问题就在于对print的调用,因为该方法是在初始化块中调用的,而初始化块与实例变量在声明处的初始化是同等级的,会按照类中出现的顺序执行;

  程序中初始化块出现在max变量的前面,所以会在max变量初始化前执行,在调用print方法时,max变量虽然已经声明,但却尚未执行初始化,其默认值为0,所以打印的结果就是0;

(2) 继承情况下的初始化顺序

继承的情况我们也都熟悉,当初始化的时候,首先会初始化父类,这是一个递归的过程。

代码如下:

public class Test {
    public static void main(String[] args) {
        System.out.println("==========================");
        new ThisClass();
        System.out.println("==========================");
        new ThisClass();
    }
}

class ThisClass extends SuperClass {
    public ThisClass() {
        System.out.println("执行ThisClass构造方法");
    }

    static {
        System.out.println("ThisClass静态代码块");
    }

    {
        System.out.println("ThisClass代码块");
    }

    private static int max = getMax();
    private int min = getMin();

    public static int getMin() {
        System.out.println("ThisClass初始化成员变量min");
        return 0;
    }

    public static int getMax() {
        System.out.println("ThisClass初始化静态成员变量max");
        return 0;
    }

}

class SuperClass {
    public SuperClass() {
        System.out.println("执行SuperClass构造方法");
    }

    static {
        System.out.println("SuperClass静态代码块");
    }
    {
        System.out.println("SuperClass代码块");
    }

    private static int max = getMax();
    private int min = getMin();

    public static int getMin() {
        System.out.println("SuperClass初始化成员变量min");
        return 0;
    }

    public static int getMax() {
        System.out.println("SuperClass初始化静态成员变量max");
        return 0;
    }
}

执行结果如下:

==========================
SuperClass静态代码块
SuperClass初始化静态成员变量max
ThisClass静态代码块
ThisClass初始化静态成员变量max
SuperClass代码块
SuperClass初始化成员变量min
执行SuperClass构造方法
ThisClass代码块
ThisClass初始化成员变量min
执行ThisClass构造方法
==========================
SuperClass代码块
SuperClass初始化成员变量min
执行SuperClass构造方法
ThisClass代码块
ThisClass初始化成员变量min
执行ThisClass构造方法

根据结果我们可以分析出:

(1)继承情况下,JVM会首先加载父类,这是一个递归的过程,直到Object类为止。在类加载中,首先为类中的静态成员变量分配内存空间,并初始化默认值,然后按照在类中的顺序执行静态代码块与静态成员变量的初始化,这个过程是从父类到子类,并且只执行一次;

(2)如果创建了类的对象,在初始化子类之前,会首先对父类的实例变量初始化默认值,然后按照在类中的顺序进行初始化,然后调用父类的构造器。如果没有创建任何对象,本环节就不会执行。

注意:我们知道,静态成员变量的初始化会在类首次加载时执行,并且只会执行一次,那么类在什么情况下会被JVM载入执行呢?

 public class JavaTest {
     public static void main(String[] args) {
 //        Super superTest1;
 //        Super superTest2 = new Super();
 //        int height = Super.height;
 //        Super.getHeight();
 /*
         try {
             Class.forName("Super");
         } catch (Exception e) {
             System.out.println("异常");
         }
 */
     }
 }

 class Super {
     static {
         System.out.println("静态代码块");
     }
     public static int height = 30;

     public static int getHeight() {
         return height;
     }
 }

完全注释掉后,运行程序,很显然,什么都没有输出;

然后我们将第3行注释取消,运行,还是什么都没有输出;

然后我们随意取消第4,5,6行任意一行,就可以打印出 "静态代码块";

由此说明,如果只是引用了类的引用,JVM是不会载入类的。JVM只是在需要某个类时才载入该类,这种需要可能是使用了该类的静态成员变量,或是调用了该类的静态方法,或是生成了该类的实例,但这并非是加载一个类的全部可能,如当加载子类时,那么父类自然也就被加载了。

同理,如果我们保留上面的注释,而取消第8行到13行的注释,那程序还是会打印出 "静态代码块"。因为JVM在加载类的时候会生成一个Class类的对象,而Class类的forName方法就是取得该类的Class对象,所以JVM会载入该类。

总结:Java变量的加载顺序,是从父类到子类,静态到非静态的过程。

等有时间,试着从JVM的角度和字节码的执行过程来研究一下Java变量的初始化顺序这个问题。

参考自:《细说Java》