簡體   English   中英

當Base類構造函數在Java中調用重寫方法時,Derived類對象的狀態

[英]State of Derived class object when Base class constructor calls overridden method in Java

請參考下面的Java代碼:

class Base{
     Base(){
         System.out.println("Base Constructor");
         method();
     }
     void method(){}    
}

class Derived extends Base{
    int var = 2;
    Derived(){
         System.out.println("Derived Constructor");  
    }

     @Override
     void method(){
        System.out.println("var = "+var);
     }
 }

class Test2{
    public static void main(String[] args) {
        Derived b = new Derived();
    }
}

看到的輸出是:

Base Constructor
var = 0
Derived Constructor

我認為var = 0的發生是因為Derived對象是半初始化的; 類似於Jon Skeet在這里所說的

我的問題是:

如果尚未創建Derived類對象,為什么會調用重寫的方法?

在什么時間點,var賦值為0?

是否存在需要此類行為的用例?

  • Derived對象創建-它只是在構造函數尚未運行。 在創建它之后,對象的類型永遠不會在Java中更改,這在所有構造函數運行之前發生。

  • 在構造函數運行之前, var被賦予默認值0作為創建對象的過程的一部分。 基本上,類型引用被設置並且表示對象的其余內存被擦除為零(概念上,無論如何 - 它可能已經被擦除為零,作為垃圾收集的一部分)

  • 這種行為至少會導致一致性,但這可能是一種痛苦。 就一致性而言,假設您有一個可變基類的只讀子類。 基類可能有一個isMutable()屬性,該屬性實際默認為true - 但子類將其isMutable()為始終返回false。 在子類構造函數運行之前,對象是可變的是奇怪的,但之后是不可變的。 另一方面,在您運行該類的構造函數之前最終在類中運行代碼的情況下,這絕對是奇怪的:(

一些指導原則:

  • 盡量不要在構造函數中做太多工作。 避免這種情況的一種方法是在靜態方法中工作,然后使靜態方法的最后部分成為構造函數調用,它只是設置字段。 當然,這意味着當你正在做工作時,你不會從多態中獲益 - 但是在構造函數調用中這樣做無論如何都是危險的。

  • 在構造函數期間盡量避免調用非final方法 - 這很可能會引起混淆。 記錄你必須 非常清楚的任何方法調用,以便任何覆蓋它們的人知道它們將在初始化完成之前被調用。

  • 如果你必須在施工期間調用一個方法,那么通常不適合在之后調用它。 如果是這種情況,請記錄並嘗試在名稱中指明它。

  • 盡量不要過度使用繼承 - 當你從一個非Object類以外的超類派生的子類時,這只會成為一個問題。繼承的設計是棘手的。

如果尚未創建Derived類對象,為什么會調用重寫的方法?

Derived類構造函數隱式調用Base類構造函數作為第一個語句。 Base類構造函數調用method() ,它調用Derived類中的重寫實現,因為這是正在創建其對象的類。 Derived類中的method()在該點看到var為0。

在什么時間點,var賦值為0?

在調用Derived類的構造函數之前,為var賦予int類型的默認值,即0。 它被分配值2隱含超構造器調用完成並在聲明之前, Derived類的構造函數開始執行。

是否存在需要此類行為的用例?

在非final類的構造函數/初始化器中使用非finalprivate方法通常是一個壞主意。 原因在您的代碼中很明顯。 如果正在創建的對象是子類實例,則這些方法可能會產生意外結果。

請注意,這與C ++不同,在C ++中,類型在構造對象時確實發生了變化,因此從基類構造函數調用虛方法不會調用派生類的覆蓋。 同樣的事情在破壞期間反過來發生。 因此,對於來到Java的C ++程序員來說,這可能是一個小陷阱。

為了解釋這種行為,應該注意Java語言規范的一些屬性:

  • 在子類的構造函數之前,總是隱式/顯式地調用超類的構造函數。
  • 來自構造函數的方法調用就像任何其他方法調用一樣; 如果方法是非final,則調用是虛擬調用,這意味着要調用的方法實現是與對象的運行時類型相關聯的實現。
  • 在構造函數執行之前,所有數據成員都使用默認值自動初始化(0表示數字基元,null表示對象,false表示布爾值)。

事件順序如下:

  1. 創建子類的實例
  2. 使用默認值初始化所有數據成員
  3. 被調用的構造函數會立即將控制權委托給相關的超類構造函數。
  4. 超級構造函數初始化其部分/全部數據成員,然后調用虛方法。
  5. 該方法由子類覆蓋,因此調用子類實現。
  6. 該方法嘗試使用子類的數據成員,假設它們已經初始化,但事實並非如此 - 調用堆棧尚未返回到子類的構造函數。

簡而言之,每當超類的構造函數調用非final方法時,我們都有進入此陷阱的潛在風險,因此不建議這樣做。 請注意,如果您堅持使用此模式,則沒有優雅的解決方案。 這里有兩個復雜且富有創意的,都需要線程同步(!):

http://www.javaspecialists.eu/archive/Issue086.html

http://www.javaspecialists.eu/archive/Issue086b.html

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM