八、 Java程序初始化的順序(一)


今天在寫構造器方法的時候,遇到了一個小問題,由這個問題引發了一連串的思考,在一個Java類中變量與類的初始化執行順序是什么樣的呢?
## 發現問題
class Student{
   private String name;
   
   void setName(String name){
      this.name = name;
   }
   String getName(){
      return name;
   }
   
   Student(){
      //this(this.name);
      this(name);
      System.out.println("題目要求寫一個無參的構造器");
   }
   
   Student(String name){
      this.name = name;
   }
   
}
class TestStudent{
   public static void main(String[] args){
      Student stu1 = new Student();
      System.out.print(stu1.getName());
      Student stu2 = new Student("老大");
      System.out.println(stu2.getName());
   }
}
此時會報錯:無法在調用超類型構造器之前引用name.

在使用構造器創建對象時,此時的成員變量name的值是否已經完成初始化,無參構造中調用它時報的這個錯意味着什么。我們本篇博客就來討論一下,一個類創建對象時到底做了哪些事?

## 思考問題
首先,對於一個類來說加載分為五個部分,分別是靜態變量,靜態代碼塊,非靜態變量,非靜態代碼塊以及構造器。

### 單個類成員加載順序
測試代碼:
class Student{
    // 靜態變量
    static String name;

    // 靜態代碼塊
    static{
        System.out.println("剛運行到靜態代碼塊時的靜態變量值:"+name);
        name = "靜態name值";
        System.out.println("靜態代碼塊結束時的靜態變量值:"+name);
    }

    //定義一個無參構造器
    Student(){
        System.out.println("剛運行到構造器時的靜態變量值:"+name);
        name = "這是一個無參的構造器";
        System.out.println("構造器結束時的靜態變量值:"+name);
    }
    //定義一個非靜態變量
    String name2;

    //定義一個非靜態代碼塊
    {
        System.out.println("剛運行到非靜態代碼塊時的非靜態變量值:"+name2);
        name2 = "非靜態name值";
        System.out.println("非靜態代碼塊結束時的非靜態變量值:"+name2);
    }
}

class TestStudent{
    public static void main(String[] args){
        Student stu = new Student();
    }
}
此時編譯代碼執行的結果是:
```
剛運行到靜態代碼塊時的靜態變量值:null
靜態代碼塊結束時的靜態變量值:靜態name值
剛運行到非靜態代碼塊時的非靜態變量值:null
非靜態代碼塊結束時的非靜態變量值:非靜態name值
剛運行到構造器時的靜態變量值:靜態name值
構造器結束時的靜態變量值:這是一個無參的構造器
```
由此可以看出,當我們聲明的類成員變量是一個靜態成員變量的時候,在調用構造器之前,我們的靜態成員變量已經生成並初始化成相應的數據類型的默認值(即此處String對象的默認值位null)。
然后在靜態代碼塊中,我們將靜態變量賦值,然后程序跳轉到非靜態變量聲明與賦值。再執行非靜態代碼塊,最后直行到程序的無參構造器。

所以,通過此程序代碼,我們得出結論:單個類的程序加載順序是:靜態變量-->靜態代碼塊-->非靜態變量-->非靜態代碼塊-->構造器。

也就是說調用構造器時,靜態與非靜態的屬性都已經完成初始化工作了,this(name)調用報錯與name屬性本身沒有關系。

### 深入思考類加載順序
既然說到加載順序,那么我們繼續完成類成員的加載順序。關於變量與代碼塊之間的關系,或者說根據我們上面的這段代碼得出這個初步的結論我們還有待商榷,因為,我們的程序加載的順序是自上而下的,也就是說,我們的得到的這個結論有可能是因為我們習慣性的排版導致的,我們聲明各部分的順序偶可能影響我們得出的結論。為了確定我們程序的嚴謹性,我們需要進一步的調整代碼的順序,來加強驗證我們代碼實驗的邏輯嚴謹性。

public class Student2 {
    // 靜態代碼塊放到前面,此時name還未聲明,所以會報錯
    static{
        System.out.println("剛運行到靜態代碼塊時的靜態變量值:"+name);
        name = "靜態name值";
        System.out.println("靜態代碼塊結束時的靜態變量值:"+name);
    }

    // 靜態變量
    static String name;


    //定義一個無參構造器
    Student2(){
        System.out.println("剛運行到構造器時的靜態變量值:"+name);
        name = "這是一個無參的構造器";
        System.out.println("構造器結束時的靜態變量值:"+name);
    }
    //定義一個非靜態代碼塊
    {
        System.out.println("剛運行到非靜態代碼塊時的非靜態變量值:"+name2);
        name2 = "非靜態name值";
        System.out.println("非靜態代碼塊結束時的非靜態變量值:"+name2);
    }
    //定義一個非靜態變量
    String name2;
}

class TestStudent{
    public static void main(String[] args){
        Student stu = new Student();
        Student2 stu2 = new Student2();
    }
}

上面代碼運行的結果:
```
Error:(6, 48) java: 非法前向引用
```
此時將代碼塊拿到變量聲明的前面我們的代碼出現了錯誤提示,這說明了我們一開始得到的結論並不嚴謹,我們這里可以得出代碼塊的執行是在變量聲明之前的。
所以,我們可以根據常識大膽的猜想,單個類程序加載的順序是靜態-->非靜態-->構造器,其中變量聲明與代碼塊的執行順序與代碼前后位置有關,並沒有嚴格的前后之分,程序員將代碼寫在前邊的的先執行。

### 驗證猜想
public class Student3 {
    //定義一個無參構造器
    Student3(){
        System.out.println("剛運行到構造器時的靜態變量值:"+name);
        name = "這是一個無參的構造器";
        System.out.println("構造器結束時的靜態變量值:"+name);
    }

    //定義一個非靜態代碼塊
    {
        name2 = "非靜態name值";
        System.out.println("非靜態代碼塊結束時的靜態變量值:"+name);
    }
    //定義一個非靜態變量
    String name2;

    // 靜態代碼塊
    static{
        System.out.println("運行到靜態代碼塊");
//        name = "靜態代碼塊里賦的值";
    }

    // 靜態變量
    static String name;
}

class TestStudent{
    public static void main(String[] args){
        Student3 stu3 = new Student3();
    }
}

上面代碼執行的結果:
```
運行到靜態代碼塊
非靜態代碼塊結束時的靜態變量值:null
剛運行到構造器時的靜態變量值:null
構造器結束時的靜態變量值:這是一個無參的構造器
```
基本驗證了我們的猜想是正確的,但是在結尾我又做了一個有趣的測試。

### 測試
public class Student3 {
    //定義一個無參構造器
    Student3(){
        System.out.println("剛運行到構造器時的靜態變量值:"+name);
        name = "這是一個無參的構造器";
        System.out.println("構造器結束時的靜態變量值:"+name);
    }

    //定義一個非靜態代碼塊
    {
        name2 = "非靜態name值";
        System.out.println("非靜態代碼塊結束時的靜態變量值:"+name);
    }
    //定義一個非靜態變量
    String name2;

    // 靜態代碼塊
    static{
        System.out.println("運行到靜態代碼塊");
        name = "靜態代碼塊里賦的值";// 按道理說,我們這里沒有聲明就直接賦值操作了
    }

    // 靜態變量
    static String name;
}

class TestStudent{
    public static void main(String[] args){
        Student3 stu3 = new Student3();
    }
}

上面代碼執行的結果:
```
運行到靜態代碼塊
非靜態代碼塊結束時的靜態變量值:靜態代碼塊里賦的值
剛運行到構造器時的靜態變量值:靜態代碼塊里賦的值
構造器結束時的靜態變量值:這是一個無參的構造器
```
也就是說在靜態代碼塊里,我們無法引用后面的靜態變量,但是我們編譯之前可以對他進行賦值,並且在后面的非靜態代碼塊里我們還可以取到里面的值,再次做出假設,這是java虛擬機在編譯時不讓向前引用,此時的變量其實已經完成了聲明初始化等一系列操作(都是存在方法區),只是通過不了編譯而已。
所以我認為,我們最早得到的結論應該才是正確的Java程序整個加載流程的順序。

這中間也可能時JDK和IDE一起努力做了點什么,但是實際上也不影響,我們的代碼塊的存在本來就是為了完成變量的初始化工作的,所以將代碼塊放到屬性聲明之前是毫無意義的操作,所以這里只是遇到了測試一下而已,實際操作中毫無意義。

## 得出結論
總結:在一個類中,初始化順序為:
1. 靜態變量,靜態變量初始化;
2. 靜態代碼塊;
3. 非靜態變量初始化;
4. 非靜態代碼塊;
5. 構造器。

##最后
到這里,我們理清楚了單個類中的各部分的加載順序,但是我們文章一開始提到的問題並沒有解決,如果構造器執行是在靜態和非靜態屬性及代碼塊之后的話,此時的成員變量應當已經有了初始化值了,再不濟成員變量還有一個初始的null值,但是這里報了無法在調用超類型構造器之前引用name。
這說明這里調用name關系到了這個類的父類構造器,所以我們后面繼續探討類加載在繼承中的加載順序,就可以解決這個問題了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM