這是一篇“站在半山腰上”的文章:沒有“山腳處”的基礎介紹,也達不到“山頂上”一般說一不二的透徹了解,隻能說是目前這個程式設計“曆史階段”下,在重讀《java程式設計思想》初始化部分内容後,結合之前的了解,再加上幾個面試向的例子,整理出來,一是達成前文的承諾,二是交流、學習、了解、提高,三是作為爬向“山頂”的一個腳印吧。
關于對象的建立和初始化,我想先把自己總結的關于初始化順序的經驗拎出來說,一是因為這是一個面試的點,多數朋友其實并不想長篇累牍的深入太多,二是因為經驗性的總結是普遍适用的,但總有一些概括不到的“特例”,對于這種結論性的東西,我想,去其形,留其神,應當是最好的,就像張三豐教張無忌武功哈哈。
這裡讨論的初始化順序,要涉及到靜态内容的初始化,是以不完全是止于對象這一層的,會涉及到類加載和類的初始化。結論有兩點:靜态優先于非靜态;屬性(及代碼塊)優先于方法。 看到這兒可以先停一下,想一想。
然後,援引書中關于對象建立的步驟,摘出來(【注1】這裡不涉及反射、clone()或反序列化等方式建立對象):
假設有一個Dog類。
1.當首次建立Dog類的對象,或首次調用Dog類的靜态方法/靜态成員(書中稱靜态域)時,java解釋器将查找類路徑,以定位Dog.class檔案。(【注2】另外書中提到:構造器雖然沒有顯式使用static,但實際上是靜态方法。以我目前的水準,并不能很好地了解這句話,我的實踐是:構造方法能夠調用非靜态成員,這一現象與靜态方法的特征沖突。)
2. 載入 Dog.class檔案,執行所有靜态初始化操作。靜态初始化隻在Class對象首次加載時執行一次。
3. 在使用new Dog()來建立對象時,首先将在堆上為該對象配置設定足夠的記憶體。(我将書中第4點合并到此處)這塊存儲空間将被清零,這自動的将Dog對象中的所有基本類型資料都設定為預設值(0),引用被置為null。(【注3】不知道我了解的對不對,但我傾向于把此處的初始化稱為預設初始化或隐式初始化)
4. 執行所有出現于字段定義處的初始化操作。(【注4】類似前述,我傾向于将此處的初始化操作稱為顯式初始化或書中提到的指定初始化,這個問題不大。最主要的問題是“所有”,必須強調,這個“所有”出現的并不嚴謹:應該是:執行除靜态成員外,其他字段的顯式初始化操作!這裡解釋一下:按照書中的理想情況,步驟2已經完整的将全部靜态初始化操作執行完畢,但是實際上(或者說特别是在面試題中…),處在類開始處的靜态對象可能先要執行new Object()的操作來完成初始化,此時,就像插曲一樣,你要先執行3-5,然後傳回頭來,繼續執行步驟2。當然,可能這樣說不直覺,沒關系,看了後例,再傳回頭來看就明白了。)
5. 執行構造器。(【注5】如書中叙,如果涉及繼承,會比較麻煩,同見後例)
至此,堆中的對象就建立并初始化完成了。我沒有補充太多類加載的内容(比如說通過符号引用判斷類是否被加載過等等),一個是JVM虛拟機和記憶體機制我懂的不多,二是不想再引入其他複雜度來幹擾這部分内容的了解了,以後系統學習JVM時候,會繼續整理相關内容。
同時,看到這裡,之前的兩條結論,應該已經忘得差不多了。下面通過幾個執行個體,具體說明。
例:
public class StaticTest {
public static void main(String[] args) {
System.out.println("main方法");
st.function();
}
static StaticTest st = new StaticTest();
static {
System.out.println("靜态代碼塊");
}
{
System.out.println("構造代碼塊");
}
public StaticTest() {
System.out.println("構造方法");
System.out.println("構造方法中 a="+a+", b="+b);
}
public void function() {
System.out.println("main方法中 a="+a+", b="+b);
System.out.println("main方法執行完畢");
}
int a =;
static int b=;
}
希望你先手寫出答案,再繼續向下看。
// Output:
構造代碼塊
構造方法
構造方法中 a=, b=
靜态代碼塊
main方法
main方法中 a=, b=
main方法執行完畢
結果是否和你的答案有出入? 如是,推薦你打斷點debug,那樣會很清楚。下面我描述一下程式的執行流程:
1、要執行main方法,需要定位并加載 StaticTest.class
2、類加載時,開始順序執行靜态初始化(包括靜态成員、靜态代碼塊)
3、首先執行
static StaticTest st = new StaticTest();
( 這裡就是前文【注4】提到的情況:雖然類中還有其他靜态成員需要初始化,但是想要完成 st 的初始化,必須要先去 new StaticTest(),執行一些看似與靜态初始化無關的“支線任務”。這就是我希望你把結論中提到的 “靜态優先于非靜态”去其形,留其神的原因。)
3-1、執行 new StaticTest(),在堆中為這個對象配置設定空間,并将此空間清0,執行預設初始化(特别注意:預設初始化之後, a=0;b=0 ,很重要!)
3-2、順序執行非靜态成員(含構造塊)的顯式初始化。本例中,先執行
{
System.out.println("構造代碼塊");
}
再執行
此時 a 被指派為 1 . 再次強調: 靜态成員 b 此時隻執行了預設初始化,賦予了初值 0 ,在 new 的過程中,不會執行顯式初始化而被指派為1!
至此,執行構造方法前的“前置任務”完成了。
3-3、執行構造方法
至此,new 操作完成, 靜态成員 st 完成初始化。
4、繼續進行靜态成員的初始化,執行
static {
System.out.println("靜态代碼塊");
}
5、完成最後一個靜态成員的初始化:
static int b=;
此時,b 被指派為 2 .
至此,StaticTest類中全部靜态成員已完成初始化,執行main方法的“前置任務”已經完成。(希望你認識到:main方法是靜态方法,這也是結論中“屬性及代碼塊優先于方法”的一處佐證)
6、執行 main 方法。
以上就是例1 。
例1 是沒有繼承關系的例子,如果加上繼承關系,那麼情況會稍稍複雜。結論是:1. 先加載父類(先執行父類靜态初始化),再加載子類(後執行子類靜态初始化) 2. 先執行個體化父類,再執行個體化子類。同樣,執行個體說明:
例2:
public class StaticTest extends Father{
public static void main(String[] args) {
System.out.println("main方法");
st.function();
}
//便于了解子類靜态初始化和父類對象執行個體化時機
static {
System.out.println("子類加載,子類靜态初始化開始");
}
static StaticTest st = new StaticTest();
static {
System.out.println("靜态代碼塊");
}
{
System.out.println("構造代碼塊");
}
public StaticTest() {
System.out.println("構造方法");
System.out.println("構造方法中 a="+a+", b="+b);
}
public void function() {
System.out.println("main方法中 a="+a+", b="+b);
System.out.println("main方法執行完畢");
}
int a =;
static int b=;
}
class Father {
public Father() {
System.out.println("執行了父類構造");
}
{
System.out.println("父類構造塊");
}
static {
System.out.println("父類加載,父類靜态初始化開始");
}
}
可以看到,我建立了新的繼承體系,并在子類初始化靜态對象 st 前,添加了一個靜态塊,目的是便于了解子類靜态初始化和父類對象執行個體化時機。
輸出如下:
//Output:
父類加載,父類靜态初始化開始
子類加載,子類靜态初始化開始
父類構造塊
執行了父類構造
構造代碼塊
構造方法
構造方法中 a=, b=
靜态代碼塊
main方法
main方法中 a=, b=
main方法執行完畢
這個例子就沒有什麼費解的地方了,記住結論就可以了,不再詳述。
如果想檢驗自己了解的如何,請看這篇文章,
http://www.cnblogs.com/maowh/p/3729971.html
看起來複雜,但隻要你真正做到了“留其神”,那就都是紙老虎!