1. 程式人生 > >深度思考Java成員變數的初始化

深度思考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語言的設計不好的地方