1. 程式人生 > >一題搞定static關鍵字

一題搞定static關鍵字

> 基礎不牢,地動山搖 開篇一道題,考察程式碼執行順序: ```java 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` ```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` ```java 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種情況(參考《深入理解Java虛擬機器(第三版)周志明》P359)觸發: 1. 使用 new 關鍵字例項化物件的時候 2. 讀取或設定一個型別的靜態欄位([常量](被final修飾的基礎型別或String型別 "常量(ConstantValue)")除外) 3. 呼叫一個型別的靜態方法 4. 使用反射呼叫類的時候 5. 當初始化類的時候,如果發現父類還沒有進行過初始化,則先觸發其父類初始化 6. 虛擬機器啟動時,會先初始化主類(包含`main()`方法的那個類) 7. 當初次呼叫 MethodHandle 例項時,初始化該 MethodHandle 指向的方法所在的類。 8. 如果介面中定義了預設方法(default 修飾的介面方法),該介面的實現類發生了初始化,則該介面要在其之前被初始化 其中的2,3條目是被`static`程式碼觸發的。 其實初始化階段就是執行類構造器``[ ](clinit並不是一個單詞,課代表認為應該是classinitial的縮寫 "'clinit'") 方法的過程,這個方法是編譯器自動生成的,裡面收集了`static`修飾的所有類變數的賦值動作和靜態語句塊(static{} 塊),並且保留這些程式碼出現的先後順序。 根據條目5,JVM 會保證在子類的``方法執行前,父類的``方法已經執行完畢。 小結一下:訪問類變數或靜態方法,會觸發類的初始化,而類的初始化就是執行``,也就是執行 `static` 修飾的賦值動作和`static{}`塊,並且 JVM 保證先執行父類初始化,再執行子類初始化。 由此得出第二條優先順序順序: * 父類的`static`程式碼 > 子類的`static`程式碼 ## 3.static 程式碼只執行一次 我們都知道,`static`程式碼(靜態方法除外)只執行一次。 你有沒有想過,這個機制是如何保證的呢? 答案是:雙親委派模型。 JDK8 及之前的雙親委派模型是: 應用程式類載入器 → 擴充套件類載入器 → 啟動類載入器 平時開發中寫的類,預設都是由 應用程式類載入器載入,它會委派給其父類:擴充套件類載入器。而擴充套件類載入器又會委派給其父類:啟動類載入器。只有當父類載入器反饋無法完成這個載入請求時,子載入器才會嘗試自己去完成載入,這個過程就是雙親委派。三者的父子關係並不是通過繼承,而是通過組合模式實現的。 該過程的實現也很簡單,下面展示關鍵實現程式碼: ```java 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. ``和`` 前面已經介紹了編譯器自動生成的類構造器:``方法,它會收集`static`修飾的所有類變數的賦值動作和靜態語句塊(static{} 塊)並保留程式碼的出現順序,它會在類初始化時執行 相應的,編譯器還會生成一個``方法,它會收集例項欄位的賦值動作、初始化語句塊({}塊)和構造器(Constructor)中的程式碼,並保留程式碼的出現順序,它會在 new 指令之後接著執行 所以,當我們new 一個類時,如果JVM未載入該類,則先對其進行初始化,再進行例項化。 至此,第三條優先順序規則也就呼之欲出了: * 靜態程式碼(static{}塊、靜態欄位賦值語句) > 初始化程式碼({}塊、例項欄位賦值語句) ## 5. 規律實踐 將前文的三條規則合併,總結出如下兩條: 1.靜態程式碼(static{}塊、靜態欄位賦值語句) > 初始化程式碼({}塊、例項欄位賦值語句) > 建構函式程式碼 2.父類的`static`程式碼 > 子類的`static`程式碼 根據前文總結,初始化程式碼和建構函式程式碼被編譯器收集到了``中,靜態程式碼被收集到了``中,所以再次對上述規律做合併: **父類`` > 子類`` > 父類 `` > 子類 ``** 對應到開篇的問題,我們來實踐一下: 當執行`new Child()`時,new關鍵字觸發了 Child 類的初始化 ,JVM 發現其有父類,則先初始化 Parent 類,開始執行Parent類的``方法,然後執行 Child 類的``方法(還記得``裡面收集了什麼嗎?)。 然後開始例項化 一個Child類的物件,此時準備執行 Child 的``方法,發現它有父類,優先執行父類的``方法,然後再執行子類的``(還記得``裡面收集了什麼嗎?)。 相信看到這裡,各位心中已經對開篇的問題有答案了,不妨先手寫一下輸出順序,然後寫程式碼親自驗證一下。 ## 結束語 平時開發中經常用到`static`,每次寫的時候,心裡總會打兩個問號,我為什麼要用`static`?不用行不行?這正應了開篇的第一句話: >基礎不牢,地動山搖 通過本文可以看出,`static`的應用遠遠不止類變數,靜態方法那麼簡單。在經典的單例模式中,你將看到`static`的各種用法,下一篇就寫如何**花式**編寫單例模式。 附上答案: ![答案](https://img2020.cnblogs.com/blog/1181064/202007/1181064-20200701091216291-992002638.png) ---