首頁  >  文章  >  Java  >  一次性帶你弄懂java裡的static關鍵字

一次性帶你弄懂java裡的static關鍵字

醉折花枝作酒筹
醉折花枝作酒筹轉載
2021-08-04 17:49:462127瀏覽

相信有不少同學遇到這類問題,可能查過資料之後接著就忘了,再次遇到還是答不對。接下來經過4個步驟,帶大家拆解這段程式碼的執行順序,並藉此總結規律。

開篇一題,檢視程式碼執行順序:

public class Parent {
    static {
        System.out.println("Parent static initial block");
    }

    {
        System.out.println("Parent initial block");
    }

    public Parent() {
        System.out.println("Parent constructor block");

    }
}

public class Child extends Parent {
    static {
        System.out.println("Child static initial block");
    }

    {
        System.out.println("Child initial block");
    }
    
    private Hobby hobby = new Hobby();

    public Child() {
        System.out.println("Child constructor block");
    }
}

public class Hobby {
    static{
        System.out.println("Hobby static initial block");
    }

    public Hobby() {
        System.out.println("hobby constructor block");
    }
}

當執行new Child()時,上述程式碼輸出什麼?

相信有不少同學遇到這類問題,可能查過資料之後接著就忘了,再次遇到還是答不對。接下來課程代表透過4個步驟,帶大家拆解這段程式碼的執行順序,並藉此總結規律。

1.編譯器優化了啥?

下面兩段程式碼對比一下編譯前後的變化:

#編譯前的Child.java

public class Child extends Parent {
    static {
        System.out.println("Child static initial block");
    }
    {
        System.out.println("Child initial block");
    }
    
    private Hobby hobby = new Hobby();
    
    public Child() {
        System.out.println("Child constructor block");
    }
}

編譯後的Child.class

public class Child extends Parent {
    private Hobby hobby;

    public Child() {
        System.out.println("Child initial block");
        this.hobby = new Hobby();
        System.out.println("Child constructor block");
    }

    static {
        System.out.println("Child static initial block");
    }
}

透過對比可以看到,編譯器把初始化區塊和實例欄位的賦值操作,移動到了建構函數程式碼之前,並且保留了相關程式碼的先後順序。事實上,如果建構函式有多個,初始化程式碼也會被複製多份移動過去。

據此可以得到第一條優先權順序:

  • 初始化程式碼 > 建構函式碼

2.static 有啥作用?

類別的載入過程可粗略分為三個階段:載入-> 連結-> 初始化

初始化階段可被8種情況週志明》P359 "觸發類別初始化的8種情況")觸發:

  • 使用new 關鍵字實例化物件的時候

  • 讀取或設定一個類型的靜態欄位(常數" )除外)

  • 呼叫一個類型的靜態方法

  • #使用反射呼叫類別的時候

  • 當初始化類別的時候,如果發現父類別還沒有進行過初始化,則先觸發其父類別初始化

  • #虛擬機啟動時,會先初始化主類別(包含main( )方法的那個類別)

  • 當初次呼叫MethodHandle 實例時,初始化該MethodHandle 所指向的方法所在的類別。

  • 如果介面中定義了預設方法(default 修飾的介面方法),該介面的實作類別發生了初始化,則該介面要在其之前被初始化

其中的2,3條目是被static程式碼觸發的。

其實初始化階段就是執行類別建構器583d030be372af71281df966e84181a5  方法的過程,這個方法是編譯器自動產生的,裡面收集了static修飾的所有類別變數的賦值動作和靜態語句區塊(static{} 區塊),並且保留這些程式碼出現的先後順序。

根據條目5,JVM 會保證在子類別的583d030be372af71281df966e84181a5方法執行前,父類別的583d030be372af71281df966e84181a5方法已經執行完畢。

小結一下:存取類別變數或靜態方法,會觸發類別的初始化,而類別的初始化就是執行583d030be372af71281df966e84181a5,也就是執行static 修飾的賦值動作和static{}區塊,且JVM 保證先執行父類別初始化,再執行子類別初始化。

由此得出第二個優先權順序:

  • 父類別的static程式碼>子類別的static程式碼

3.static 程式碼只執行一次

我們都知道,static程式碼(靜態方法除外)只執行一次。

你有沒有想過,這個機制是如何保證的呢?

答案是:雙親委派模型。

JDK8 及之前的雙親委派模型是:

應用程式類別加載器→ 擴充類別載入器→ 啟動類別載入器

平常開發中寫的類,預設都是由應用程式類別載入器加載,它會委派給其父類:擴充類別載入器。而擴充類別載入器又會委派給其父類別:啟動類別載入器。只有當父類別載入器回饋無法完成這個載入請求時,子載入器才會嘗試自己去完成載入,這個過程就是雙親委派。三者的父子關係並不是透過繼承,而是透過組合模式實現的。

流程的實作也很簡單,以下展示關鍵實作程式碼:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先检查该类是否被加载过
    // 如果加载过,直接返回该类
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类抛出ClassNotFoundException
            // 说明父类无法完成加载请求
        }

        if (c == null) {
            // 如果父类无法加载,转由子类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

結合註解相信大家很容易看懂。

由雙親委派的程式碼可知,同一個類別載入器下,一個類別只能載入一次,也就限定了它只能被初始化一次。所以類別中的static程式碼(靜態方法除外)只在類別初始化時執行一次

4. 7e51f00a783d7eb8f68358439dee7daf和583d030be372af71281df966e84181a5

前面已經介紹了編譯器自動產生的類別構造器:583d030be372af71281df966e84181a5方法,它會收集static修飾的所有類別變數的賦值動作和靜態語句區塊(static{} 區塊)並保留程式碼的出現順序,它會在類別初始化時執行

對應的,編譯器也會產生一個7e51f00a783d7eb8f68358439dee7daf方法,它會收集實例欄位的賦值動作、初始化語句區塊({}區塊)和建構器(Constructor)中的程式碼,並保留程式碼的出現順序,它會在new 指令之後接著執行

所以,當我們new 一個類別時,如果JVM未載入該類,則先對其進行初始化,再進行實例化。

至此,第三條優先權規則也就呼之欲出了:

  • 靜態程式碼(static{}區塊、靜態欄位賦值語句) >初始化程式碼({}區塊、實例欄位賦值語句)

#5.規則實作

#將前文的三條規則合併,總結出如下兩條:

1.靜態程式碼(static{}區塊、靜態欄位賦值語句) > 初始化程式碼({}區塊、實例欄位賦值語句) > 建構函式碼

2.父類別的static程式碼> 子類別的static程式碼

根據前文總結,初始化程式碼和建構函式程式碼被編譯器收集到了7e51f00a783d7eb8f68358439dee7daf中,靜態程式碼被收集到了583d030be372af71281df966e84181a5中,所以再次對上述規則做合併:

父類別583d030be372af71281df966e84181a5 > 子類別583d030be372af71281df966e84181a5 > 父類別7e51f00a783d7eb8f68358439dee7daf > 子類別7e51f00a783d7eb8f68358439dee7daf

對應到開篇的問題,我們來實作一下:

當執行new Child()時,new關鍵字觸發了Child 類別的初始化,JVM 發現其有父類,則先初始化Parent 類,開始執行Parent類別的583d030be372af71281df966e84181a5方法,然後執行Child 類的583d030be372af71281df966e84181a5方法(還記得583d030be372af71281df966e84181a5裡面收集了什麼嗎?)。

然後開始實例化一個Child類的對象,此時準備執行Child 的7e51f00a783d7eb8f68358439dee7daf方法,發現它有父類,優先執行父類的7e51f00a783d7eb8f68358439dee7daf方法,然後再執行子類的7e51f00a783d7eb8f68358439dee7daf(還記得7e51f00a783d7eb8f68358439dee7daf裡面收集了什麼嗎?)。

相信看到這裡,各位心中已經對開篇的問題有答案了,不妨先手寫一下輸出順序,然後寫程式碼親自驗證一下。

結束語

平常開發中常用到static,每次寫的時候,心裡總是會打兩個問號,我為什麼要用static?不用行不行?

透過本文可以看出,static的應用遠遠不只類別變量,靜態方法那麼簡單。在經典的單例模式中,你會看到static的各種用法,下一篇就寫如何花式寫單例模式。

以上是一次性帶你弄懂java裡的static關鍵字的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除