本文提供一個項目中的錯誤實例,提供對其觀察和分析,揭示出Java語言實例化一個對象具體過程,最后總結出設計Java類的一個重要規則。通過閱讀本文,可以使Java程序員理解Java對象的構造過程,從而設計出更加健壯的代碼。本文適合Java初學者和需要提高的Java程序員閱讀。
程序擲出了一個異常
作者曾經在一個項目里面向項目組成員提供了一個抽象的對話框基類,使用者只需在子類中實現基類的一個抽象方法來畫出顯示數據的界面,就可使項目內的對話框具有相同的風格。具體的代碼實現片斷如下(為了簡潔起見,省略了其他無關的代碼):
這個類在有的代碼中工作得很好,但一個同事在使用時,程序卻擲出了一個NullPointerException違例!經過比較,找出了工作正常和不正常的程序的細微差別,代碼片斷分別如下:
一、正常工作的代碼:
二、工作不正常的代碼:
你看出來兩段代碼之間的差別了嗎?對了,兩者的差別僅僅在于類變量jTextFieldName的初始化時間。經過跟蹤,發現在執行panel.add(jTextFieldName)語句之時,jTextFieldName確實是空值。
我們知道,Java允許在定義類變量的同時給變量賦初始值。系統運行過程中需要創建一個對象的時候,首先會為對象分配內存空間,然后在“先于調用任何方法之前”根據變量在類內的定義順序來初始化變量,接著再調用類的構造方法。那么,在本例中,為什么在變量定義時便初始化的代碼反而會出現空指針違例呢?
對象的創建過程和初始化
實際上,前面提到的“變量初始化發生在調用任何方法包括構造方法之前”這句話是不確切的,當我們把眼光集中在單個類上時,該說法成立;然而,當把視野擴大到具有繼承關系的兩個或多個類上時,該說法不成立。
對象的創建一般有兩種方式,一種是用new操作符,另一種是在一個Class對象上調用newInstance方法;其創建和初始化的實際過程是一樣的:
首先為對象分配內存空間,包括其所有父類的可見或不可見的變量的空間,并初始化這些變量為默認值,如int類型為0,boolean類型為false,對象類型為null;
然后用下述5個步驟來初始化這個新對象:
1)分配參數給指定的構造方法;
2)如果這個指定的構造方法的第一個語句是用this指針顯式地調用本類的其它構造方法,則遞歸執行這5個步驟;如果執行過程正常則跳到步驟5;
3)如果構造方法的第一個語句沒有顯式調用本類的其它構造方法,并且本類不是Object類(Object是所有其它類的祖先),則調用顯式(用super指針)或隱式地指定的父類的構造方法,遞歸執行這5個步驟;如果執行過程正常則跳到步驟5;
4)按照變量在類內的定義順序來初始化本類的變量,如果執行過程正常則跳到步驟5;
5)執行這個構造方法中余下的語句,如果執行過程正常則過程結束。
這一過程可以從下面的時序圖中獲得更清晰的認識:

對分析本文的實例最重要的,用一句話說,就是“父類的構造方法調用發生在子類的變量初始化之前”。可以用下面的例子來證明:
運行這段代碼,它的執行結果如下:
Store
Animal
Cat
Petstore
從結果中可以看出,在創建一個Petstore類的實例時,首先調用了它的父類Store的構造方法;然后試圖創建并初始化變量cat;在創建cat時,首先調用了Cat類的父類Animal的構造方法;其后才是Cat的構造方法主體,最后才是Petstore類的構造方法的主體。
所以那些有子類實現的方法盡量不要放在父類的構造函數中,一定要注意設計好構造函數。
程序擲出了一個異常
作者曾經在一個項目里面向項目組成員提供了一個抽象的對話框基類,使用者只需在子類中實現基類的一個抽象方法來畫出顯示數據的界面,就可使項目內的對話框具有相同的風格。具體的代碼實現片斷如下(為了簡潔起見,省略了其他無關的代碼):
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允許在定義類變量的同時給變量賦初始值。系統運行過程中需要創建一個對象的時候,首先會為對象分配內存空間,然后在“先于調用任何方法之前”根據變量在類內的定義順序來初始化變量,接著再調用類的構造方法。那么,在本例中,為什么在變量定義時便初始化的代碼反而會出現空指針違例呢?
對象的創建過程和初始化
實際上,前面提到的“變量初始化發生在調用任何方法包括構造方法之前”這句話是不確切的,當我們把眼光集中在單個類上時,該說法成立;然而,當把視野擴大到具有繼承關系的兩個或多個類上時,該說法不成立。
對象的創建一般有兩種方式,一種是用new操作符,另一種是在一個Class對象上調用newInstance方法;其創建和初始化的實際過程是一樣的:
首先為對象分配內存空間,包括其所有父類的可見或不可見的變量的空間,并初始化這些變量為默認值,如int類型為0,boolean類型為false,對象類型為null;
然后用下述5個步驟來初始化這個新對象:
1)分配參數給指定的構造方法;
2)如果這個指定的構造方法的第一個語句是用this指針顯式地調用本類的其它構造方法,則遞歸執行這5個步驟;如果執行過程正常則跳到步驟5;
3)如果構造方法的第一個語句沒有顯式調用本類的其它構造方法,并且本類不是Object類(Object是所有其它類的祖先),則調用顯式(用super指針)或隱式地指定的父類的構造方法,遞歸執行這5個步驟;如果執行過程正常則跳到步驟5;
4)按照變量在類內的定義順序來初始化本類的變量,如果執行過程正常則跳到步驟5;
5)執行這個構造方法中余下的語句,如果執行過程正常則過程結束。
這一過程可以從下面的時序圖中獲得更清晰的認識:

對分析本文的實例最重要的,用一句話說,就是“父類的構造方法調用發生在子類的變量初始化之前”。可以用下面的例子來證明:
// Petstore.java class 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(); } } |
運行這段代碼,它的執行結果如下:
Store
Animal
Cat
Petstore
從結果中可以看出,在創建一個Petstore類的實例時,首先調用了它的父類Store的構造方法;然后試圖創建并初始化變量cat;在創建cat時,首先調用了Cat類的父類Animal的構造方法;其后才是Cat的構造方法主體,最后才是Petstore類的構造方法的主體。
尋找程序產生例外的原因 現在回到本文開始提到的實例中來,當程序創建一個ChildDlg2的實例時,根據super(null, “Title”)語句,首先執行其父類BaseDlg的構造方法;在BaseDlg的構造方法中調用了createClientPanel()方法, 再來看ChildDlg1,它的jTextFieldName的初始化代碼寫在了createClientPanel()方法內部的開始處,這樣它就能保證在使用之前得到正確的初始化,因此這段代碼工作正常。 解決問題的兩種方式 通過上面的分析過程可以看出,要排除故障,最簡單的方法就是要求項目組成員在繼承使用BaseDlg類,實現createClientPanel()方法時,凡方法內部要使用的變量必須首先正確初始化,就象ChildDlg1一樣。然而,把類變量放在類方法內初始化是一種很不好的設計行為,它最適合的地方就是在變量定義塊和構造方法中。 在本文的實例中,引發錯誤的實質并不在ChildDlg2上,而在其父類BaseDlg上,是它在自己的構造方法中不適當地調用了一個待實現的抽象方法。 從概念上講,構造方法的職責是正確初始化類變量,讓對象進入可用狀態。而BaseDlg卻賦給了構造方法額外的職責。 本文實例的更好的解決方法是修改BaseDlg類:
新的BaseDlg類增加了一個initGUI()方法,程序員可以這樣使用這個類:
總結 類的構造方法的基本目的是正確初始化類變量,不要賦予它過多的職責。 設計類構造方法的基本規則是:用盡可能簡單的方法使對象進入就緒狀態;如果可能,避免調用任何方法。在構造方法內唯一能安全調用的是基類中具有final屬性的方法或者private方法(private方法會被編譯器自動設置final屬性)。final的方法因為不能被子類覆蓋,所以不會產生問題。 | |||