Java中類的例項化過程變數的初始化順序,以及常見筆試程式閱讀題分析

類是在任何static成員被訪問時載入的(構造器也是static方法)。類的整個載入過程包括載入、驗證、準備、解析、初始化5個階段。我這裡只討論我們在筆試題中比較關心的、影響程式輸出的部分。
類載入:
在準備階段,static變數在方法區被分配記憶體,然後記憶體被初始化零值(注意和static變數初始化的區別)。
在初始化階段,執行類構造器<clinit>()方法(注意和例項構造器<init>()方法不同)。虛擬機器會保證子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行。
在執行<clinit>()方法時,按照類定義中static變數的賦值語句和static程式碼段的書寫順序,依次執行。
子類呼叫基類的靜態方法時,相當於基類呼叫自己的靜態方法,所以子類的static不會初始化。例子如下:
Child.sMethodBase(); // 類的定義在最後面
這一句的執行結果為:
基類initPrint2 靜態變數s4:null 基類靜態方法sMethodBase 靜態變數s4:基類靜態變數s4
建立物件:
虛擬機器在遇到new指令時,首先檢查類是否載入過,在類載入檢查通過後,虛擬機器為物件分配記憶體,分配完記憶體後會將記憶體空間初始化為零值(不包括物件頭)。所以物件的例項欄位在初始化之前就有了零值。
執行new指令之後會接著執行例項構造器<init>方法,這時才開始物件的初始化。
進入構造器時,如果有基類,會進入基類的無參構造器(或者用super()顯式指定的基類構造器)。在構造之前,先按照例項欄位和非static程式碼段的書寫順序,依次初始化,最後執行構造器的語句。
super()語句要按基類的次序,放在構造器最前面,否則編譯器會報錯。
建立子類物件的例子如下:
Child child = new Child("s");
輸出結果為:
基類initPrint2 靜態變數s4:null 子類initPrint2 靜態變數s2:null 基類initPrint1 例項變數s3:null 基類initPrint1 靜態變數s4:基類靜態變數s4 基類構造器 int i 子類initPrint1 例項變數s1:null 子類initPrint1 靜態變數s2:子類靜態變數s2 子類構造器
可見,確實是先載入類(第1、2行發生在static變數的初始化階段),然後再建立物件(第3行及以後)。建立的過程也是從父類到子類,先是非static變數的初始化(初始化前已經有預設值了,如第3行和第6行所示),然後執行構造器語句。
上面用到的類的定義如下:
class Base { private int x3 = initPrint1(); public String s3 = "基類例項變數s3"; private static int x4 = initPrint2(); private static String s4 = "基類靜態變數s4"; private int initPrint1() { System.out.println("基類initPrint1 例項變數s3:" + s3); System.out.println("基類initPrint1 靜態變數s4:" + s4); return 11; } private static int initPrint2() { System.out.println("基類initPrint2 靜態變數s4:" + s4); return 21; } public Base(int i) { System.out.println("基類構造器 int i"); } public void callName() { System.out.println(s3); } public static void sMethodBase() { System.out.println("基類靜態方法sMethodBase 靜態變數s4:"+s4); } }
class Child extends Base { private int x1 = initPrint1(); public String s1 = "子類例項變數s1"; private static int x2 = initPrint2(); private static String s2 = "子類靜態變數s2"; private int initPrint1() { System.out.println("子類initPrint1 例項變數s1:" + s1); System.out.println("子類initPrint1 靜態變數s2:" + s2); return 11; } private static int initPrint2() { System.out.println("子類initPrint2 靜態變數s2:" + s2); return 21; } public Child(String s) { super(1); System.out.println("子類構造器"); } public void callName() { System.out.println(s1); } public static void sMethodChild() { System.out.println("子類靜態方法sMethodChild 靜態變數s2:"+s2); } }
方法和欄位的重寫
另一個基礎的問題是子類對父類的override。
方法的重寫有執行時繫結的效果,子類例項如果重寫了基類的方法,即使向上轉型為基類,呼叫的仍是子類的方法。而且在方法中的欄位也會優先認為是子類的欄位。
但是欄位並沒有執行時繫結一說,向上轉型後呼叫的就是基類的欄位。
同時靜態方法與類關聯,並不是與單個物件關聯,它也沒有執行時繫結。
class Base { public String s1 = "基類例項變數s1"; private static String s2 = "基類靜態變數s2"; public void f() { System.out.println("基類方法"); } } class Child extends Base { public String s1 = "子類例項變數s1"; private static String s2 = "子類靜態變數s2"; public void f() { System.out.println("子類方法"); } }
對於上面的兩個類,當如下使用時:
Child child = new Child(); System.out.println(((Base)child).s1); ((Base)child).f();
輸出的結果為:
基類例項變數s1 子類方法
需要補充說明的是,private的方法雖然可以重寫,但已經不是傳統意義上的override,因為父類的private方法對子類不可見,所以子類重寫的函式被認為是新函式,在父類函式中將子類向上轉型時,呼叫的仍是父類的private方法,這是在類載入的解析階段就確定的。
class Base { public String s1 = "基類例項變數s1"; private void f() { System.out.println("基類方法"); } public static void main(String[] args) { Child child = new Child(); System.out.println(((Base)child).s1); ((Base)child).f(); } } class Child extends Base { public String s1 = "子類例項變數s1"; public void f() { System.out.println("子類方法"); } }
Base的main函式執行結果為:
基類例項變數s1 基類方法
解析階段中確定唯一呼叫版本的方法有static方法、private方法、例項構造器和父類方法4類,滿足“編譯器可知,執行期不變”的要求。
綜合題
最後我們來看一道牛客網上的題目:
public class Base { private String baseName = "base"; public Base() { callName(); } public void callName() { System. out. println(baseName); } static class Sub extends Base { private String baseName = "sub"; public void callName() { System. out. println (baseName) ; } } public static void main(String[] args) { Base b = new Sub(); } }
程式的輸出結果是什麼呢?
null (子類重寫了父類的public方法,public例項方法屬於執行時繫結的方法,實際呼叫時,傳入的this引用的是一個子類物件,所以定位到了子類的函式,而父類構造時,子類還未構造,子類例項變數還沒有初始化,為零值)。如果將Base裡面的public callName()修改為private callName()結果會是什麼呢?
base(子類並沒有真正重寫父類的callName()方法,它們是兩個不同的方法,private方法在類載入階段解析,滿足“編譯器可知,執行期不變”的要求,和呼叫者的實際型別無關,根據上下文資訊,父類構造器中的方法被指定為父類中的私有方法)。