我們知道在通過new關鍵字生成對像時必然會調用構造函數,構造函數的簡繁情況會直接影響實例對象的創建是否繁瑣。在項目開發中,我們一般都會制訂構造函數盡量簡單,盡可能不拋異常,盡量不做複雜算法等規範,那如果一個構造函數確實複雜了會怎麼樣?我們來看一段代碼:
public class Client{
public static void main(Stringargs){
Server s=new SimpleServer(1000);
}
}
//定義一個服務
abstract class Server{
public final static int DEFAULT_PORT=40000;
public Server(){
//獲得子類提供的端口號
int port=getPort();
System.out.println("端口號:"+port);
/*進行監聽動作*/
}
//由子類提供端口號,並做可用性檢查
protected abstract int getPort();
}
class SimpleServer extends Server{
private int port=100;
//初始化傳遞一個端口號
public SimpleServer(int_port){
port=_port;
}
//檢查端口號是否有效,無效則使用默認端口,這裡使用隨機數模擬
@Override
protected int getPort(){
return Math.random()>0.5?port:DEFAULT_PORT;
}
}
該代碼是一個服務類的簡單模擬程序,Server類實現了服務器的創建邏輯,子類只要在生成實例對像時傳遞一個端口號即可創建一個監聽該端口的服務,該代碼的意圖如下:
通過SimpleServer的構造函數接收端口參數。
子類的構造函數默認調用父類的構造函數。
父類構造函數調用子類的getPort方法獲得端口號。
父類構造函數建立端口監聽機制。
對像創建完畢,服務監聽啟動,正常運行。
貌似很合理,再仔細看看代碼,確實也和我們的意圖相吻合,那我們嘗試多次運行看看,輸出結果要麼是「端口號:40000」,要麼是「端口號:0」,永遠不會出現「端口號:100」或是「端口號:1000」,這就奇怪了,40000還好說,但那個0是怎麼冒出來的呢?代碼在什麼地方出現問題了?
要解釋這個問題,我們首先要說說子類是如何實例化的。子類實例化時,會首先初始化父類(注意這裡是初始化,可不是生成父類對像),也就是初始化父類的變量,調用父類的構造函數,然後才會初始化子類的變量,調用子類自己的構造函數,最後生成一個實例對象。瞭解了相關知識,我們再來看上面的程序,其執行過程如下:
子類SimpleServer的構造函數接收int類型的參數:1000。
父類初始化常變量,也就是DEFAULT_PORT初始化,並設置為40000。
執行父類無參構造函數,也就是子類的有參構造中默認包含了super()方法。
父類無參構造函數執行到"int port=getPort()"方法,調用子類的getPort方法實現。
子類的getPort方法返回port值(注意,此時port變量還沒有賦值,是0)或DEFAULT_PORT(此時已經是40000)了。
父類初始化完畢,開始初始化子類的實例變量,port賦值100。
執行子類構造函數,port被重新賦值為1000。
子類SimpleServer實例化結束,對像創建完畢。
終於清楚了,在類初始化時getPort方法返回的port值還沒有賦值,port只是獲得了默認初始值(int類的實例變量默認初始值是0),因此Server永遠監聽的是40000端口了(0端口是沒有意義的)。這個問題的產生從淺處說是由類元素初始化順序導致的,從深處說是因為構造函數太複雜而引起的。構造函數用作初始化變量,聲明實例的上下文,這都是簡單的實現,沒有任何問題,但我們的例子卻實現了一個複雜的邏輯,而這放在構造函數里就不合適了。
問題知道了,修改也很簡單,把父類的無參構造函數中的所有實現都移動到一個叫做start的方法中,將SimpleServer類初始化完畢,再調用其start方法即可實現服務器的啟動工作,簡潔而又直觀,這也是大部分JEE服務器的實現方式。
注意 構造函數簡化,再簡化,應該達到「一眼洞穿」的境界。