本文提供一個項目中的錯誤執行個體,提供對其觀察和分析,揭示出Java語言執行個體化一個對象具體過程,最後總結出設計Java類的一個重要規則。通過閱讀本文,可以使Java程式員理解Java對象的構造過程,從而設計出更加健壯的代碼。本文適合Java初學者和需要提高的Java程式員閱讀。
程式擲出了一個違例
作者曾經在一個項目裡面向項目群組成員提供了一個抽象的對話方塊基類,使用者只需在子類中實現基類的一個抽象方法來畫出顯示資料的介面,就可使項目內的對話方塊具有相同的風格。具體的代碼實現片斷如下(為了簡潔起見,省略了其他無關的代碼):
public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); } private JPanel createHeadPanel() { ... // 建立對話方塊頭部 } // 建立對話方塊用戶端區域,交給子類實現 protected abstract JPanel createClientPanel(); private JPanel createButtonPanel { ... // 建立按鈕地區 }}
這個類在有的代碼中工作得很好,但一個同事在使用時,程式卻擲出了一個NullPointerException違例!經過比較,找出了工作正常和不正常的程式的細微差別,代碼片斷分別如下:
一、工作正常的代碼:
public class ChildDlg1 extends BaseDlg { JTextField jTextFieldName; public ChildDlg1() { super(null, "Title"); } public JPanel createClientPanel() { jTextFieldName = new JTextField(); JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代碼 return panel; } ...}ChildDlg1 dlg = new ChildDlg1(frame, "Title"); // 外部的調用
二、工作不正常的代碼:
public class ChildDlg2 extends BaseDlg { JTextField jTextFieldName = new JTextField(); public ChildDlg2() { super(null, "Title"); } public JPanel createClientPanel() { JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代碼 return panel; } ...}ChildDlg2 dlg = new ChildDlg2(); // 外部的調用
你看出來兩段代碼之間的差別了嗎?對了,兩者的差別僅僅在於類變數jTextFieldName的初始化時間。經過跟蹤,發現在執行panel.add(jTextFieldName)語句之時,jTextFieldName確實是空值。
我們知道,Java允許在定義類變數的同時給變數賦初始值。系統運行過程中需要建立一個對象的時候,首先會為對象分配記憶體空間,然後在“先於調用任何方法之前”根據變數在類內的定義順序來初始設定變數,接著再調用類的構造方法。那麼,在本例中,為什麼在變數定義時便初始化的代碼反而會出現null 指標違例呢?
對象的建立過程和初始化
實際上,前面提到的“變數初始化發生在調用任何方法包括構造方法之前”這句話是不確切的,當我們把眼光集中在單個類上時,該說法成立;然而,當把視野擴大到具有繼承關係的兩個或多個類上時,該說法不成立。
對象的建立一般有兩種方式,一種是用new操作符,另一種是在一個Class對象上調用newInstance方法;其建立和初始化的實際過程是一樣的:
首先為對象分配記憶體空間,包括其所有父類的可見或不可見的變數的空間,並初始化這些變數為預設值,如int類型為0,boolean類型為false,物件類型為null;
然後用下述5個步驟來初始化這個新對象:
1)分配參數給指定的構造方法;
2)如果這個指定的構造方法的第一個語句是用this指標顯式地調用本類的其它構造方法,則遞迴執行這5個步驟;如果執行過程正常則跳到步驟5;
3)如果構造方法的第一個語句沒有顯式調用本類的其它構造方法,並且本類不是Object類(Object是所有其它類的祖先),則調用顯式(用super指標)或隱式地指定的父類的構造方法,遞迴執行這5個步驟;如果執行過程正常則跳到步驟5;
4)按照變數在類內的定義順序來初始化本類的變數,如果執行過程正常則跳到步驟5;
5)執行這個構造方法中餘下的語句,如果執行過程正常則過程結束。
這一過程可以從下面的時序圖中獲得更清晰的認識:
對分析本文的執行個體最重要的,用一句話說,就是“父類的構造方法調用發生在子類的變數初始化之前”。可以用下面的例子來證明:
// Petstore.javaclass Animal { Animal() { System.out.println("Animal"); }}class Cat extends Animal { Cat() { System.out.println("Cat"); }}class Store { Store() { System.out.println("Store"); }}public class Petstore extends Store{ Cat cat = new Cat(); Petstore() { System.out.println("Petstore"); } public static void main(String[] args) { new Petstore(); }}
運行這段代碼,它的執行結果如下:
StoreAnimalCatPetstore
從結果中可以看出,在建立一個Petstore類的執行個體時,首先調用了它的父類Store的構造方法;然後試圖建立並初始設定變數cat;在建立cat時,首先調用了Cat類的父類Animal的構造方法;其後才是Cat的構造方法主體,最後才是Petstore類的構造方法的主體。
尋找程式產生例外的原因
現在回到本文開始提到的執行個體中來,當程式建立一個ChildDlg2的執行個體時,根據super(null, “Title”)語句,首先執行其父類BaseDlg的構造方法;在BaseDlg的構造方法中調用了createClientPanel()方法,這個方法是抽象方法並且被子類ChildDlg2實現了,因此,實際調用的方法是ChildDlg2中的createClientPanel()方法(因為Java裡面採用“動態綁定”來綁定所有非final的方法);createClientPanel()方法使用了ChildDlg2類的執行個體變數jTextFieldName,而此時ChildDlg2的變數初始化過程尚未進行,jTextFieldName是null值!所以,ChildDlg2的構造過程擲出一個NullPointerException也就不足為奇了。
再來看ChildDlg1,它的jTextFieldName的初始化代碼寫在了createClientPanel()方法內部的開始處,這樣它就能保證在使用之前得到正確的初始化,因此這段代碼工作正常。
解決問題的兩種方式
通過上面的分析過程可以看出,要排除故障,最簡單的方法就是要求項目群組成員在繼承使用BaseDlg類,實現createClientPanel()方法時,凡方法內部要使用的變數必須首先正確初始化,就象ChildDlg1一樣。然而,把類變數放在類方法內初始化是一種很不好的設計行為,它最適合的地方就是在變數定義塊和構造方法中。
在本文的執行個體中,引發錯誤的實質並不在ChildDlg2上,而在其父類BaseDlg上,是它在自己的構造方法中不適當地調用了一個待實現的抽象方法。
從概念上講,構造方法的職責是正確初始化類變數,讓對象進入可用狀態。而BaseDlg卻賦給了構造方法額外的職責。
本文執行個體的更好的解決方案是修改BaseDlg類:
public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); } /** 建立對話方塊執行個體後,必須調用此方法來布局使用者介面 */ public void initGUI() { this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); } private JPanel createHeadPanel() { ... // 建立對話方塊頭部 } // 建立對話方塊用戶端區域,交給子類實現 protected abstract JPanel createClientPanel(); private JPanel createButtonPanel { ... // 建立按鈕地區 }}
新的BaseDlg類增加了一個initGUI()方法,程式員可以這樣使用這個類:
ChildDlg dlg = new ChildDlg();dlg.initGUI();dlg.setVisible(true);
總結
類的構造方法的基本目的是正確初始化類變數,不要賦予它過多的職責。
設計類構造方法的基本規則是:用儘可能簡單的方法使對象進入就緒狀態;如果可能,避免調用任何方法。在構造方法內唯一能安全調用的是基類中具有final屬性的方法或者private方法(private方法會被編譯器自動化佈建final屬性)。final的方法因為不能被子類覆蓋,所以不會產生問題。