類加載和初始化順序
這個博客是我看Thinking In Java的筆記與記錄
簡單介紹類加載:
在很多編程語言中,程序是作為啟動過程的一部分立刻被加載出來的,然後是初始化工作,然後是程序開始。 這些語言必須嚴格控制初始化的過程,這樣才能保證static變量的初始化不會出問題。比如像C++,就有可能出現一個static變量在初始化的過程中,需要另一個static變量已經成功初始化並已經有效,不然就會有問題。而Java不會出現這樣的問題。因為它采用一個比較特別的方法去加載。
萬物在Java中都是object,每個類的編譯代碼都在自己的那個獨立的文件中,也就是每個.class文件,這些文件或者說是編譯代碼只有在代碼第一次被使用的時候會加載。
什麽叫第一次被使用?第一個這個類的對象被構造出來,或者是類中的static方法或者是static變量被調用或者是使用了,這兩個情況隨便發生一個,類加載就發生了。(但其實一個對象要被構造出來,就要調用這個類的構造函數,而構造函數也是static,所以其實可以說只要有static方法或者變量的調用或使用,類就被加載)
然後有個一定要清楚的是,初次使用的時候,也是static變量初始化的時候。在類加載的時候,所有的static對象會按照你在代碼中定義的順序一次初始化,可以看作static對象的初始化和類加載是一起進行的。當然static對象只會初始化一次。
類變量的自動初始化和初始化順序:
Java為了保證類變量在使用之前已經得到初始化,有個自動賦值的機制。就如果你在定義這個類變量的時候沒有給它初始化,new出來這個對象後這些類變量會被自動初始化賦值。基本數據類型會被自動賦值為默認值(這裏就不說明了),對象的引用會被自動賦值為null。
構造器的其中一個用處之一,就是為類變量提供初始化的服務。但註意,構造器的初始化也就是構造器中為類變量賦值的這個動作,並不能阻止類變量自動賦值的機制,也就是說,在進入構造器之前,類變量其實已經賦值完畢了。
public class Counter { int i; Counter() { i = 7; } // ... }
如果你new一個Counter對象,那麽i先被自動賦值為0,再是進入構造器,被賦值為7.也就是說,類變量的初始化(自動)在構造器之前就完成了。
在一個類中,類變量的初始化順序就是代碼中他們的位置,這些類變量初始化完之後,就到構造器中的工作。這個例子可以很好得體現類變量的初始化順序。
class Window { Window(int marker) { print("Window(" + marker + ")"); } }
class House { Window w1 = new Window(1); // Before constructor House() { // Show that we’re in the constructor: print("House()"); w3 = new Window(33); // Reinitialize w3 } Window w2 = new Window(2); // After constructor void f() { print("f()"); } Window w3 = new Window(3); // At end }
public class OrderOfInitialization { public static void main(String[] args) { House h = new House(); h.f(); // Shows that construction is done } }
/* Output: Window(1) Window(2) Window(3) House() Window(33) f()
static對象的初始化:
static對象只有一塊公共的內存,static關鍵字只能用於類變量,所以static也會被自動初始化,初始化的規則也和普通的類變量一樣,這裏就不重復講了。
然後就是static對象的初始化是在類加載的時候就進行的,下面看個比較長的例子,來深刻理解下static對象的初始化:
class Bowl { Bowl(int marker) { print("Bowl(" + marker + ")"); } void f1(int marker) { print("f1(" + marker + ")"); } } class Table { static Bowl bowl1 = new Bowl(1); Table() { print("Table()"); bowl2.f1(1); } void f2(int marker) { print("f2(" + marker + ")"); } static Bowl bowl2 = new Bowl(2); } class Cupboard { Bowl bowl3 = new Bowl(3); static Bowl bowl4 = new Bowl(4); Cupboard() { print("Cupboard()"); bowl4.f1(2); } void f3(int marker) { print("f3(" + marker + ")"); } static Bowl bowl5 = new Bowl(5); } public class StaticInitialization { public static void main(String[] args) { print("Creating new Cupboard() in main"); new Cupboard(); print("Creating new Cupboard() in main"); new Cupboard(); table.f2(1); cupboard.f3(1); } static Table table = new Table(); static Cupboard cupboard = new Cupboard(); }
輸出是:
/* Output: Bowl(1) Bowl(2) Table() f1(1) Bowl(4) Bowl(5) Bowl(3) Cupboard() f1(2) Creating new Cupboard() in main Bowl(3) Cupboard() f1(2) Creating new Cupboard() in main Bowl(3) Cupboard() f1(2) f2(1) f3(1) *///:~
現在我們來一步一步說明下這些輸出:
首先是類StaticInitialization類中的main函數,這是程序啟動的入口。而這個main函數是個static的對象,也就是說,現在是第一次運行了StaticInitialization類的一個static對象,這個類便要加載,然後static變量按順序初始化。 我們看到,這個類的static對象有:main函數,table(Table類的對象引用),cupboard(Cupboard的對象引用),也就是說要加載StaticInitialization類,需要先初始化table和cupboard。
而要也就是說現在要創建Table對象還有Cupboard對象。
new Table(),這個語句就是第一次構造Table對象,或者說要調用Table的構造器,static函數(第一次),所以構造這個對象,要先加載Table類和初始化Table類中的static對象:static Bowl bowl1和static Bowl bowl2
而這個時候我們又要第一次構造Bowl對象,調用了Bowl的static構造器,又是要加載Bowl類,初始化Bowl類的static對象,Bowl類中沒有static對象,然後加載就完成,進入構造對象階段。先初始化類變量,先賦值為默認值,再執行賦值語句。但Bowl中沒有非static的類變量,所以接著進入Bowl的構造器——於是便有了我們的第一和第二個輸出——Bowl(1)和Bowl(2)。
好的現在Table的兩個static變量初始化完成了,也就是說Table類的加載完成,進入構造對象階段,Table類也沒有能夠初始化的非static類變量,所以進入Table的構造器——第三個輸出——Table()。構造器中還有個語句是bowl2.f1(1);,這個是個非static的方法,於是第四個輸出——f1(1)
然後是new Cupboard()語句,一樣的,因為是第一次構造Cupboard對象,所以先加載Cupboard類和初始化Cupboard類中的static對象——static Bowl bowl4和static Bowl bowl5。由於這個時候已經不是第一次用到Bowl類的對象,所以不用加載Bowl類了,也就是跳過類加載階段,直接進入構造對象階段,一樣沒有類變量要初始化,直接構造器——第五和第六個輸出——Bowl(4)和Bowl(5)。然後Cupboard的類加載就結束了,進入構造Cupboard的階段,先是初始化類變量,先賦值為默認值,也就是bowl3 = null; 然後再執行賦值語句,也就是Bowl bowl3 = new Bowl(3),為這個普通類變量了bowl3賦值。Bowl已經加載類並且沒有普通類變量,所以直接構造器打印——第六個輸出——bowl(3)。Cupboard的類變量初始化之後,便進入Cupboard的構造器,一個pirnt語句和bowl4.f1(1),於是有了第七個和第八個輸出——Cupboard()和f1(2)
好了,到了這裏,StaticInitialization類的兩個static變量都初始化成功了,也就是說StaticInitialization類的加載完成了。(現在思路回到這個類調用main方法那)因為這個類加載是因為運行了static方法,而不是new對象(調用構造器),所以不用構造對象。接下來就是進入到main函數裏面了。先第九個輸出——Creating new Cupboard in main()。然後main方法裏面new了個Cupboard對象,因為這裏已經不是第一次用這個Cupboard類,所以不用類加載,直接構造Cupboard對象。一樣先初始化類變量,然後構造器,所以有了第十個、十一個和十二個輸出——Bowl(3)和Cupboard()還有f1(2)。後面又new了個Cupboard對象和直接調用對象的方法,後面幾個輸出就不講解了。
所以這裏來稍微總結下普通類的加載與初始化順序:
- 用到static變量或者是static的方法(註意構造器也是static方法),第一次用new創建某個類的對象(也就是第一次用static的構造器方法),Java編譯器會找到這個類的.class文件也就是編譯代碼(在classpath中),然後加載這個類。
- static變量的初始化立刻開始,可以看作類中static變量的初始化也是類加載的一部分。
- 如果你是用new Dog()的方法,也就是創建了一個對象,或者說是static的構造器方法觸發了構造對象這個動作,那麽首先會在堆上為這個對象分配足夠的內存,然後這塊內存會被清零。這麽做的後果就是,類變量會被自動地賦值為默認值,像0和null。
- 然後是執行類變量的賦值語句,像是Leg leg = new Leg()類似的,就是為類變量賦值。
- 類變量的初始化結束後,便進入構造器中,跑構造器中的代碼。
(我覺得3,4步可以就簡單看作是:進行類變量的初始化工作,定義時沒賦值的就自動賦默認值,有手動賦值的就執行賦值操作~)
涉及繼承的類加載和初始化順序:
首先要知道的一個概念是:當你創建一個子類的的對象的時候,裏面其實也包含了一個基類的對象。(可能不止一個基類)所以,基類正確的初始化就變成了一個很重要的工作。Java怎樣保證基類的初始化呢?通過基類的構造器。Java會自動在子類構造器中第一時間call基類的構造器。(當然是自動調用無參的那個構造器,如果你自己重載了一個有參的構造器,然後又沒有寫無參的構造器,那麽你要在子類構造器的第一行用super顯示調用你自己寫的那個構造器)
好的知道這些信息後,就直接看這個例子吧:
package www.com.thinkInJava.reusdingClasses; class Insect { private int i = 9; protected int j; protected int h = printInit("Insect.h initialized and j=" + j); Insect() { System.out.println("i = " + i + ", j = " + j); j = 39; } private static int x1 = printInit("static Insect.x1 initialized"); static int printInit(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect { private int k = printInit("Beetle.k initialized"); public Beetle() { System.out.println("k = " + k); System.out.println("j = " + j); } private static int x2 = printInit("static Beetle.x2 initialized"); public static void main(String[] args) { System.out.println("Beetle constructor"); Beetle b = new Beetle(); } }
輸出是:
/** static Insect.x1 initialized static Beetle.x2 initialized Beetle constructor Insect.h initialized and j=0 i = 9, j = 0 Beetle.k initialized k = 47 j = 39 **/
好了現在也來一步一步分析下輸出,理解下涉及繼承的類加載和初始化順序:
首先,運行程序,是從main函數進去的,也就是先調用了Beetle類的static方法——main方法。第一次用static方法,編譯器找到Beetle類的編譯代碼也就是Beetle.class,然後加載這個類。但在加載這個類的過程中,發現了extends關鍵字,所以按邏輯關系,編譯器現在得先加載它的基類:Insect類。
加載Insect類的過程和加載普通類的過程一樣,找到.class文件,加載,並立刻初始化static對象。(如果Insect還有基類,那麽也要先加載基類)Insect有個private static int x1 = printlnt(static Insect.x1 initialized),這個static field的初始化用到了一個static的方法,於是有了第一句輸出——static Insect.x1 initialized。
然後Insect的類加載過程就完成了,現在可以繼續進行Beetle類的加載。 這裏補一句,之所以要先加載基類,是為了保證加載子類的時候,static對象的初始化時如果需要用到基類的static對象,不會出現基類的static對象還沒有初始化或者invalid的。 所以現在就開始初始化Beetle類中的static對象。private static int x2 = printInit("static Beetle.x2 initialized"); 於是有了第二句輸出——static Beetle.x2 initialized。然後Beetle的加載過程就完成了,因為這個類加載是由main方法引起的,所以沒有構造對象的對象。所以是運行main方法裏剩下的內容。所以第三句輸出——Beetle constructor。然後下一句是Beetle b = new Beetle()。但由於剛剛已經第一次用了Beetle類中是static方法(main),這裏已經不是第一次了,所以沒有類加載的過程,直接進入構造對象的過程。
構造對象的過程和普通的類似乎有點不一樣:首先是分配足夠的內存給Beetle對象,然後再清空,也就是把Beetle類中的類變量的值自動賦值為默認值,也就是k = 0。然後調用基類的構造器(我覺得不一樣就是在這裏,因為上面的普通類的過程是賦默認值後,運行了賦值語句才到構造器的)。這裏是自動調用,你也可以用super顯示調用。由於基類剛剛加載過了,所以這裏不用進行類的加載,而是直接進行基類對象的構造:分配足夠的內存,清空自動賦默認值,也就是Insect中的i = 0,j = 0,h = 0,x1 = 0。然後再執行相應的賦值語句——第四句輸出——Insect.h initialized and j=0。 這裏是執行了類變量h的賦值語句protected int h = printInt("Insect.h initialized and j = " + j); ,還有執行對i的賦值語句,private int i = 9。 然後再進入基類的構造器中,執行構造器中的代碼——第五句輸出——i = 9, j = 0。
基類對象的初始化完成了,構造完成了,就繼續子類對象的構造,剛剛做到所有類變量賦默認值,現在是執行類變量的賦值語句——第六句輸出——Beetle.k initialized。 然後才是Beetle的構造器中的代碼——第七句輸出——k =47 (回車)j = 39
也來總結一下,父類Animal,子類Dog,new Dog():
- 先是第一次用Dog的對象,也是static的構造方法的第一次使用,所以加載Dog.class,發現有基類,加載Animal.class
- 立刻初始化Animal的static field,完成Animal的類加載,然後繼續Dog的類加載
- 因為是new語句,所以開始構造Dog對象,先分配足夠內存,然後清0,相當於為所有的Dog中的類變量賦值為默認值,0啊null這些。
- 然後調用基類的構造器,進行基類對象的構造,一樣先為Animal類中的類變量賦值為默認值,然後執行賦值語句,為類變量賦值。然後進入Animal的構造器中,執行代碼。
- Animal對象的初始化與構造完成,繼續Dog的初始化與對象的構造,執行Dog中類變量的賦值語句,最後進入Dog的構造器中執行相應的代碼。
大概的類加載和類變量的初始化順序有點頭緒了,再具體的細節還有一些問題,待到學習到JVM的時候再深入去理解與學習。
類加載和初始化順序