《編寫高質量代碼:改善 Java 程序的 151 個建議》第 3 章聚焦類、對象與方法設計規範。本文精選建議 31-35,結合源碼分析與 JVM 原理,揭示看似簡單卻暗藏陷阱的編碼細節。
雖然 Java 允許在接口中通過匿名內部類實現方法(如常量初始化),但這違背了接口「契約」本質。接口應只定義規範,不綁定具體實現,否則會導致實現類穩定性喪失,增加重構風險。
interface B {
public static final S s = new S() { // 匿名內部類實現
public void doSomeThing() { System.out.println("接口中實現"); }
};
}
鐵律:接口僅聲明常量、抽象方法、繼承父接口,絕無具體實現。
JVM 類初始化階段會先分配靜態字段內存空間(賦默認值),再按代碼順序執行顯式賦值與靜態塊。調換聲明與賦值順序會導致最終值違背直覺:
static { i = 100; } // 先執行
public static int i = 1; // 後執行,最終 i=1
根因:JVM 分兩階段處理——準備階段分配內存賦默認值,初始化階段按順序執行賦值語句。
子類定義與父類同名靜態方法屬於「隱藏」而非「覆寫」。靜態方法綁定於表面類型(聲明類型),非靜態方法綁定於實際類型(對象類型):
Base base = new Sub(); base.doAnything(); // 輸出:子類非靜態方法(動態綁定) base.doSomething(); // 輸出:父類靜態方法(靜態綁定)
隱藏與覆寫本質區別:@Override 僅適用覆寫;隱藏旨在屏蔽父類行為,覆寫旨在延伸父類職責。
構造函數調用鏈中,父類構造器執行時子類字段尚未初始化。若父類構造器調用抽象方法(由子類實現),將讀取到子類字段默認值而非預期值:
abstract class Server {
public Server() { int port = getPort(); } // 此時子類 port=0
protected abstract int getPort();
}
class SimpleServer extends Server {
private int port = 100;
public SimpleServer(int _port) { port = _port; }
protected int getPort() { return Math.random() > 0.5 ? port : DEFAULT_PORT; }
}
解決方案:將複雜邏輯遷移至單獨的 init/start 方法,構造器僅做字段初始化。
構造器中 new 其他類極易引發循環依賴導致 StackOverflowError。典型場景:框架父類構造器創建攔截器,攔截器構造器反向創建子類實例:
class Father { public Father() { new Other(); } }
class Other { public Other() { new Son(); } }
class Son extends Father { } // new Son() → Father() → Other() → Son() 死循環
最佳實踐:構造器僅初始化本類字段,外部依賴延遲至工廠方法或初始化器註入。
這 5 條建議貫穿「契約精神」「初始化順序」「多態本質」「職責單一」「依賴解耦」五大設計哲學。優秀代碼不在於技巧炫耀,而在於對語言機制的敬畏與對維護成本的預判。