您好,登錄后才能下訂單哦!
java中static關鍵字如何使用,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
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個步驟,帶大家拆解一下這段代碼的執行順序,并借此總結規律。
下面兩段代碼對比一下編譯前后的變化:
編譯前的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"); } }
通過對比可以看到,編譯器把初始化塊和實例字段的賦值操作,移動到了構造函數代碼之前,并且保留了相關代碼的先后順序。事實上,如果構造函數有多個,初始化代碼也會被復制多份移動過去。
據此可以得出第一條優先級順序:
初始化代碼 > 構造函數代碼
類的加載過程可粗略分為三個階段:加載 -> 鏈接 -> 初始化
初始化階段可被8種情況周志明》P359 "觸發類初始化的8種情況")觸發:
使用 new 關鍵字實例化對象的時候
讀取或設置一個類型的靜態字段(常量")除外)
調用一個類型的靜態方法
使用反射調用類的時候
當初始化類的時候,如果發現父類還沒有進行過初始化,則先觸發其父類初始化
虛擬機啟動時,會先初始化主類(包含main()方法的那個類)
當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。
如果接口中定義了默認方法(default 修飾的接口方法),該接口的實現類發生了初始化,則該接口要在其之前被初始化
其中的2,3條目是被static代碼觸發的。
其實初始化階段就是執行類構造器<clinit> 方法的過程,這個方法是編譯器自動生成的,里面收集了static修飾的所有類變量的賦值動作和靜態語句塊(static{} 塊),并且保留這些代碼出現的先后順序。
根據條目5,JVM 會保證在子類的<clinit>方法執行前,父類的<clinit>方法已經執行完畢。
小結一下:訪問類變量或靜態方法,會觸發類的初始化,而類的初始化就是執行<clinit>,也就是執行 static 修飾的賦值動作和static{}塊,并且 JVM 保證先執行父類初始化,再執行子類初始化。
由此得出第二條優先級順序:
父類的static代碼 > 子類的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代碼(靜態方法除外)只在類初始化時執行一次
前面已經介紹了編譯器自動生成的類構造器:<clinit>方法,它會收集static修飾的所有類變量的賦值動作和靜態語句塊(static{} 塊)并保留代碼的出現順序,它會在類初始化時執行
相應的,編譯器還會生成一個<init>方法,它會收集實例字段的賦值動作、初始化語句塊({}塊)和構造器(Constructor)中的代碼,并保留代碼的出現順序,它會在 new 指令之后接著執行
所以,當我們new 一個類時,如果JVM未加載該類,則先對其進行初始化,再進行實例化。
至此,第三條優先級規則也就呼之欲出了:
靜態代碼(static{}塊、靜態字段賦值語句) > 初始化代碼({}塊、實例字段賦值語句)
將前文的三條規則合并,總結出如下兩條:
1.靜態代碼(static{}塊、靜態字段賦值語句) > 初始化代碼({}塊、實例字段賦值語句) > 構造函數代碼
2.父類的static代碼 > 子類的static代碼
根據前文總結,初始化代碼和構造函數代碼被編譯器收集到了<init>中,靜態代碼被收集到了<clinit>中,所以再次對上述規律做合并:
父類<clinit>
> 子類<clinit>
> 父類 <init>
> 子類 <init>
對應到開篇的問題,我們來實踐一下:
當執行new Child()時,new關鍵字觸發了 Child 類的初始化 ,JVM 發現其有父類,則先初始化 Parent 類,開始執行Parent類的<clinit>方法,然后執行 Child 類的<clinit>方法(還記得<clinit>里面收集了什么嗎?)。
然后開始實例化 一個Child類的對象,此時準備執行 Child 的<init>方法,發現它有父類,優先執行父類的<init>方法,然后再執行子類的<init>(還記得<init>里面收集了什么嗎?)。
看完上述內容,你們掌握java中static關鍵字如何使用的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。