從幾道面試題看物件的初始化

無聊夫斯基發表於2018-08-05

這是無意間在網上看到的一道考物件導向的一道題,乍眼一看發現做不出來。好東西就要來分享一下,請看題。

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();
        System.out.println(b);
    }
}
複製程式碼

求程式最後的輸出。

估摸著這道題考了多型,類的初始化以及成員載入的順序。

先入為主,以上的程式碼中有一個main方法,作為一個類的入口,執行main方法觸發了類載入的過程。

類初始化

對於什麼時候開始類的初始化,以下摘自深入理解java虛擬機器第七章? 在類未被初始化過的前提下。

  1. 遇到new getstatic putstatic invokestatic這四個指令時。
  2. 使用java.lang.reflect對類進行反射呼叫。
  3. 初始化時發現父類未進行初始化,則先觸發父類的初始化。
  4. 虛擬機器啟動時,使用者需要制定一個執行主類(main方法的那個類)。

對於一個類的初始化階段就是執行類構造器方法的過程。方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的。

Java虛擬機器會對做以下規定。

  1. 虛擬機器會保證子類的方法執行之前父類的已經執行。
  2. 介面中不能使用靜態語句塊,介面初始化的時候不用關心父類介面的初始化。只有父類介面定義的變數使用時才初始化。
  3. 多個執行緒初始化一個類,虛擬機器只允許一個執行緒執行方法,其他執行緒都阻塞等待直到方法執行完畢。

物件初始化

初始化後,接著就開始執行類中的方法。在執行new Sub(),它會觸發物件的初始化。在java中有多種建立物件的方式,new是最直觀最常用的那種。其他還有

  1. 反射機制(Class.newInstance()、 Constructor.newInstance())
  2. Clone方法(obj.clone())
  3. 反序列化

在物件被建立時,虛擬機器會為其分配空間來存放物件本身的例項變數還有從父類繼承過來的變數,在這個過程中還會為各個變數設定初始預設值。 接著就會進行物件的初始化。一般分為3個步驟,跟類初始化其實差不太多。

  1. 例項變數初始化
  2. 例項程式碼塊初始化
  3. 建構函式初始化

對於1、2並沒有嚴格意義上的執行順序,誰在前面誰先跑。但是3一定是在1、2之後。為什麼?構造方法的目的就是為了在建立物件時給成員變數賦初始值,如果成員都沒定義,那建構函式就沒有意義了。但是對於1、2點來說,誰在上面誰就先執行。

在new Sub()觸發物件的初始化後,會先呼叫Sub類的無參構造方法,因為Sub類中沒有顯式宣告一個無參的構造方法,那就呼叫它預設的無參構造。子類的構造方法中都有一個super()去呼叫父類的無參構造。

在Java中有以下這麼幾個規則。

  1. 每個Java類中都會有一個建構函式,如果沒有顯示的定義建構函式,那麼就會隱式的給他一個無參構造方法。
  2. 在類例項化之前必須先例項化它的父類以保證所建立例項的完整性。
  3. Java強制要求Object物件(Object是Java的頂層物件,沒有超類)之外的所有物件建構函式的第一條語句必須是超類建構函式的呼叫語句或者是類中定義的其他的建構函式,如果我們既沒有呼叫其他的建構函式,也沒有顯式呼叫超類的建構函式,那麼編譯器會為我們自動生成一個對超類建構函式的呼叫。其實也就相當於是在建構函式中使用this呼叫當前類其他的建構函式,或者是使用super呼叫父類的建構函式。

至於以上3條規則的原因我還沒找到官方的解釋,找到再補上。

看到這裡,或許可以瞄一眼另一道題

class Person {
    String name = "No name";
    public Person(String nm) {
        name = nm;
    }
}
class Employee extends Person {
    String empID = "0000";
    public Employee(String id) {
        empID = id;
    }
}
public class Test {
    public static void main(String args[]) {
        Employee e = new Employee("123");
        System.out.println(e.empID);
    }
}
複製程式碼

這題考的是繼承關係中,父類子類構造器初始化的問題。這邊再拷一下上面的一句話 -> 子類的構造方法中都有一個super()去呼叫父類的無參構造。再瞄一眼程式碼,WTF 爸爸的無參構造去哪了?如果父類沒有無參的建構函式的情況,子類需要在自己的建構函式中顯式呼叫父類的建構函式。

根據上面的規則,程式碼就變成了

public Sub() {
    // super(); 隱式的呼叫父類的無參構造
    callName(); // -> System.out.println(baseName);
    private String baseName = "sub";
    
}
複製程式碼

所以結果一目瞭然了吧,在執行callName()的時候baseName還未初始化,執行到這一步的時候要搞清楚成員變數的初始化順序。

根據以上一波文件的尋找發現構造器初始化順序大概就是 父類靜態 -> 子類靜態 -> 父類成員 -> 父類構造 -> 子類成員 -> 子類構造

最後給一個網上找到的一檢測題來練一波手。wota:Java初始化順序

以後碰上這類面試題往上套就是了。通過理解這個面試題,目的是更深入的瞭解了類初始化和物件初始化的過程。

學習的最終目的並不是為了面試,面試只是一個激勵學習的動機。把握面試題,享受學習新知識的樂趣。

參考:

《深入理解Java虛擬機器》

相關文章