下面這個程式有兩個不可變的值類(value class),值類即其實例表示值的類。第乙個類用整數座標來表示平面上的乙個點,第二個類在此基礎上新增了一點顏色。主程式將建立和列印第二個類的乙個例項。那麼,下面的程式將列印出什麼呢?
class point
protected string makename()
public final string tostring()
}public class colorpoint extends point
protected string makename()
public static void main(string args)
}main方法建立並列印了乙個colorpoint例項。println方法呼叫了該colorpoint例項的tostring方法,這個方法是在point中定義的。tostring方法將直接返回name域的值,這個值是通過呼叫makename方法在point的構造器中被初始化的。對於乙個point例項來說,makename方法將返回[x,y]形式的字串。對於乙個colorpoint例項來說,makename方法被覆寫為返回[x,y]:color形式的字串。在本例中,x是4,y是2,color的purple,因此程式將列印[4,2]:purple,對嗎?不,如果你執行該程式,就會發現它列印的是[4,2]:null。這個程式出什麼問題了呢?
這個程式遭遇了例項初始化順序這一問題。要理解該程式,我們就需要詳細跟蹤該程式的執行過程。下面是該程式注釋過的版本的列表,用來引導我們了解其執行順序:
class point
protected string makename()
public final string tostring()
}public class colorpoint extends point
protected string makename()
public static void main(string args)
}在下面的解釋中,括號中的數字引用的就是在上述注釋版本的列表中的注釋標號。首先,程式通過呼叫colorpoint構造器建立了乙個colorpoint例項(1)。這個構造器以鏈結呼叫其超類構造器開始,就像所有構造器所做的那樣(2)。超類構造器在構造過程中對該物件的x域賦值為4,對y域賦值為2。然後該超類構造器呼叫makename,該方法被子類覆寫了(3)。
colorpoint中的makename方法(4)是在colorpoint構造器的程式體之前執行的,這就是問題的核心所在。makename方法首先呼叫super.makename,它將返回我們所期望的[4,2],然後該方法在此基礎上追加字串「:」和由color域的值所轉換成的字串。但是此刻color域的值是什麼呢?由於它仍處於待初始化狀態,所以它的值仍舊是預設值null。因此,makename方法返回的是字串「[4,2]:null」。超類構造器將這個值賦給name域(3),然後將控制流返回給子類的構造器。
這之後子類構造器才將「purple」賦予color域(5),但是此刻已經為時過晚了。color域已經在超類中被用來初始化name域了,並且產生了不正確的值。之後,子類構造器返回,新建立的colorpoint例項被傳遞給println方法,它適時地呼叫了該例項的tostring方法,這個方法返回的是該例項的name域的內容,即「[4,2]:null」,這也就成為了程式要列印的東西。
本謎題說明:在乙個final型別的例項域被賦值之前,存在著取用其值的可能,而此時它包含的仍舊是其所屬型別的預設值。在某種意義上,本謎題是謎題49在例項方面的相似物,謎題49是在final型別的靜態域被賦值之前,取用了它的值。在這兩種情況中,謎題都是因初始化的迴圈而產生的,在謎題49中,是類的初始化;而在本謎題中,是例項初始化。兩種情況都存在著產生極大的混亂的可能性,但是它們之間有乙個重要的差別:迴圈的類初始化是無法避免的災難,但是迴圈的例項初始化總是可以且總是應該避免的。
無論何時,只要乙個構造器呼叫了乙個已經被其子類覆寫了的方法,那麼該問題就會出現,因為以這種方式被呼叫的方法總是在例項被初始化之前執行。要想避免這個問題,就千萬不要在構造器中呼叫可覆寫的方法,直接呼叫或間接呼叫都不行[ej item 15]。這項禁令應該擴充套件至例項初始器和偽構造器(pseudoconstructors)readobject與clone。(這些方法之所以被稱為偽構造器,是因為它們可以在不呼叫構造器的情況下建立物件。)
你可以通過惰性初始化name域來訂正該問題,即當它第一次被使用時初始化,以此取代積極初始化,即當point例項被建立時初始化。
通過這種修改,該程式就可以列印出我們期望的[4,2]:purple。
class point
protected string makename()
// lazily computers and caches name on first use
public final synchronized string tostring()
}儘管惰性載入可以訂正這個問題,但是對於讓乙個值類去擴充套件另乙個值類,並且在其中新增乙個會對euqals比較方法產生影響的域的這種做法仍舊不是乙個好主意。你無法在超類和子類上都提供乙個基於值的equals方法,而同時又不違反object.equals方法的通用約定,或者是不消除在超類和子類之間進行有實際意義的比較操作的可能性[ej item 7]。
迴圈例項初始化問題對語言設計者來說是問題成堆的地方。c++是通過在構造階段將物件的型別從超類型別改變為子類型別來解決這個問題的。如果採用這種解決方法,本謎題中最開始的程式將列印[4,2]。我們發現沒有任何一種流行的語言能夠令人滿意地解決這個問題。也許,我們值得去考慮,當超類構造器呼叫子類方法時,通過丟擲乙個不受檢查的異常使迴圈例項初始化非法。
總之,在任何情況下,你都務必要記住:不要在構造器中呼叫可覆 寫的方法。在例項初始化中產生的迴圈將是致命的。該問題的解決方案就是惰性初始化[ej items 13,48]。
不要在建構函式中呼叫可重寫的方法
原因 非密封型別的構造函式呼叫其類中定義的虛方法。規則說明 呼叫虛方法時,直到執行時之前都不會選擇執行該方法的實際型別。構造函式呼叫虛方法時,可能尚未執行呼叫該方法的例項的建構函式。如何修復衝突 要修復與該規則的衝突,請不要從某型別的建構函式中呼叫該型別的虛方法。何時禁止顯示警告 不要禁止顯示此規則...
不要在建構函式和析構函式中呼叫虛函式
提到建構函式和析構函式,想必大家肯定是非常了解,但是能否在建構函式或是析構函式中呼叫虛函式呢?答案是千萬不要這麼做,這麼做不會得到大家想要的結果。首先提一下建構函式,建構函式的順序是從基類開始構造 子類,如果在基類中呼叫虛函式,由於建構函式基類中僅存在自身 或其父類,如果存在 不會根據虛函式表的規則...
C 不要在建構函式和析構函式中呼叫虛函式
這裡先執行個示例 include using namespace std class base virtual void fun virtual void fun 0 base class derived public base virtual void fun virtual void fun d...