跳至主要內容

深度思考Java成员变量的初始化


写Java代码的时候很少去关注成员变量的声明和初始化顺序,今天借此机会抛出一些问题:语言的设计者们为什么会这样设计?比如说很常见的一个问题:abstract(抽象)类不能用final进行修饰。这个问题比较好理解:因为一个类一旦被修饰成了final,那么意味着这个类是不能被继承的,而abstract(抽象)类又不能被实例化。如果一个抽象类可以是final类型的,那么这个类又不能被继承也不能被实例化,就没有存在的意义。从语言的角度来讲一个类既然是抽象类,那么它就是为了继承,所以给它标识为final是没有意义的。语言的设计者们当然不可能让这么大的一个bug产生。对于开发者而言抽象类不能修饰final可能就是一种约定俗成的规定,并没有特殊意义。我们完全可以往前想一点:为什么这么设计?

下面我所展示的一些代码实例也同样会采用我上面的一些思考方法。有一些是一些”契约“,并没有特别的缘由,可能用别的方法也是合理的。下面的代码会讲到初始化的一些策略,从实际的执行结果中得出一些结论。

代码一

public class Test1 {  
    {  
        a = 1;  
        //System.out.println(a);//这里会抛错。  
    }  
    private int a=2;//这里初始化一次,上面的动态块中也对a进行了赋值,这个时候a=?,为什么可以对a进行赋值,而不可以对a进行输出  
    public static void main(String[] args){  
        Test1 test1 = new Test1();  
        System.out.println(test1.a);  
    }  
} 

看看上面的代码一,第一个问题就是这段代码能否编译通过。结果是能编译通过。这里说明一个问题就是变量的声明和赋值是两步操作?(这句话我先保留一半,在上面
的代码中有一行代码我注释掉了,这里会抛错,对于这个问题我也没有想明白为什么。)
第一个问题解决了。那下一个问题很显然最后输出的结果是什么?答案是“2”,这里可能会有些诧异。从直观上来讲就是说明在赋值的过程中是完全按照代码的前后顺序进
行。

代码二

public class Test2 {  
      
    {  
        a = 4;  
    }  
      
    private final int a;//这里我并没有对a做初始化。  
      
    public static void main(String[] args){  
        Test2 test2 = new Test2();  
        System.out.println(test2.a);  
    }  
}

“代码二”只是在“代码一”的基础上对成员变量a多修饰了一个final,另外我并没有立即初始化。第一个问题就是这段代码能不能编译通过,答案是能。在这里展示这段代码是为了后面做铺垫,因为这段代码仍然符合上面的“契约”。

代码三

public class Test3 {  
      
    {  
        a = 4;  
    }  
      
    private static int a;  
      
    public static void main(String[] args){  
        Test3 test3 = new Test3();//注意:这里开始new了一个对象  
        System.out.println(test3.a);  
    }  
}

代码三在代码一的的基础上对于成员变量a多修饰了一个static。这里同样可以编译通过,最后输出的结果也皆大欢喜为4。这里要注意的是我是new了一个对象,而不是直接访问静态变量。

代码四

public class Test3 {  
    {  
        a = 4;  
        System.out.println(a);//这里不会报错,但是这条语句并不会执行  
    }  
    private static int a;  
      
    public static void main(String[] args){  
        System.out.println(a);  
    }  
} 

代码四在代码三的基础上把new 对象给去掉了,直接输出静态变量a。这时候就会出现非常诧异的结果0。对,你没有看错,结果是0。如果有兴趣的可以在a=4后面打印一条,会很清晰的发现并没有执行a=4那一条语句。这里先不解释,只看一下现象。

代码五

public class Test3 {  
    static{  
        a = 4;  
        //System.out.println(a);//这里会抛错。  
    }  
    private static int a;  
    public static void main(String[] args){  
        System.out.println(a);  
    }  
}

代码五和代码四和不同在于采用了静态初始化,最后的结论很简单,结果为4。这里有一个问题就是如果在a=4之后紧接着使用a就会报错。也就是说定义在声明之前的静态化块只能对声明变量进行赋值,并不能使用该变量。对于这一条规则我也不是特别理解,因为按照常理在赋值之后进行使用是一种再正常不过的事情,在这里只有记住这样一条规则。

代码六

public class Test6 {  
      
    {  
        a = 4;  
    }  
      
    private static final int a;  
      
    public static void main(String[] args){  
        System.out.println(a);  
    }  
} 

代码六是在代码一的基础上增加static final的修饰符。回到我们上面三段代码所问的问题,这次的答案是“否”,也就是说在这里是不能编译通过。在这里我估计有一部分人和我有同样的疑惑:为什么对于成员变量修饰单独修饰final或者static可以进行单独的初始化,而把两个修饰符合起来的时候就不行了呢?我们把这个问题要反过来问:如果可以这样进行初始化会产生什么问题,那么就可以知道为什么需要这样设计?

我们看代码三、代码四、代码五和代码六,这里估计会有点绕。上面也说了在代码三中的初始化块是执行了的,而代码四的初始化块没有执行,代码五的静态初始化块也执行了。所以问题归根结底就一条:静态初始化块和普通的初始化块在什么时候执行。结论就是在初始化阶段编译器会收集类中的类变量(区别实例变量)的赋值动作和静态语句块中的语句,而静态的调用并不会触发实例变量的初始化。

这里回到代码六,根据上面所得出的结论。变量a被修鉓成了static final,那么意味着有且仅有一次赋值。我们在访问a的同时,域中的a=4并未执行(根据代码四所得出的结论),这样就违背了final类型有且仅有一次赋值的这样一个约定。所以{a=4;}不管是放在声明的代码前还是声明的代码后都无法编译通过。

代码七

public class Test7 {  
      
    static{  
        a = 5;  
    }  
      
    private static final int a;  


    public static void main(String[] args){  
        System.out.println(a);  
    }  
}

代码七和代码六的不同在于使用静态化的初始化方法,并不会违背final有且仅有一次赋值这样的一个约定。

总结

上面七段代码大概阐述了一下变量的初始化顺序。大部分结果可以通过一些已有的结论推敲出来,也方便我们进行记忆。很多人可能会问:了解这些有用吗?如果只能看到我上面所写的那七段代码,可能意义并不大,在平常的编码过程中也不太可能会去那么写,即使写错了eclipse也会很明显的把错误指出来。而我得出的结论:从语言设计者的角度来思考Java语言为什么这么设计?如果这样去思考,然后再进行深度挖掘,一定可以得出一些不一样的理解,甚至可以找出java语言的设计不好的地方。