首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

《編寫高質量代碼》第 8 章異常處理精華:建議 114-117 深度解讀與實踐

     編輯:Java 綜合教學

《編寫高質量代碼》第 8 章異常處理精華:建議 114-117 深度解讀與實踐

本文摘自《編寫高質量代碼:改善 Java 程序的 151 個建議》第 8 章,聚焦異常處理四大核心建議,結合源碼分析、反模式識別與最佳實踐,幫助開發者建立正確的異常觀。

建議 114:不要在構造函數中拋出異常

三類異常回顧

  • Error:虛擬機級錯誤(OOM、StackOverflow),不可捕獲處理
  • RuntimeException:非受檢異常(NPE、越界),可捕獲可忽略
  • Exception:受檢異常(IO、SQL),必須捕獲或聲明拋出

為什麼構造器不該拋異常?

  1. Error 無法預防:JVM 級故障,程序員無能為力
  2. RuntimeException 危害隱蔽:
    class Person {
        public Person(int age) {
            if (age < 18) throw new RuntimeException("未成年");
        }
    }
    Person p = new Person(17); // 編譯通過,運行崩潰,調用方無感知
    調用方以為對象創建成功,實際引用為 null,後續調用拋 NPE,極難排查。
  3. 受檢異常破壞繼承多態:
    class Base { public Base() throws IOException { throw new IOException(); } }
    class Sub extends Base { public Sub() throws Exception { } } // 編譯錯誤:子類異常範圍比父類寬
    
    // 構造器無覆寫概念,子類必須聲明拋出父類所有異常 + 自己的異常
    // 導致裡氏替換原則失效,靈活性大幅降低
    更糟的是,子類無法在 super() 之後處理父類異常(語法要求 super() 必須首行)。

曲線救國方案

  • 工廠方法模式:static Person create(int age) { if(age<18) throw...; return new Person(age); }
  • 初始化方法:構造器不拋異常,提供 init() 拋異常
  • 構建器模式:Person.builder().age(17).build() 內部校驗

建議 115:使用 Throwable 獲取棧信息實現調用者感知

原理

public Throwable() {
    fillInStackTrace(); // 本地方法,捕獲當前線程棧幀
}
public StackTraceElement[] getStackTrace() { ... }

實戰:根據調用者返回不同結果

class Foo {
    public static boolean method() {
        StackTraceElement[] sts = new Throwable().getStackTrace();
        for (StackTraceElement st : sts) {
            if ("methodA".equals(st.getMethodName())) return true;
        }
        return false; // 或拋異常阻止非預期調用
    }
}

class Invoker {
    public static void methodA() { System.out.println(Foo.method()); } // true
    public static void methodB() { System.out.println(Foo.method()); } // false
}

應用場景

  • 離線授權校驗:僅允許特定入口調用核心邏輯
  • 安全防護:檢測調用鏈異常(如反射繞過檢查)
  • 動態行為:同一方法根據調用上下文執行不同邏輯

建議 116:異常只為異常服務,不可作為正常流程控制

反模式:用異常做業務判斷

// 判斷枚舉是否包含某值
public static > boolean contains(Class clz, String name) {
    try { Enum.valueOf(clz, name); return true; }
    catch (RuntimeException e) { return false; } // 濫用異常控制流
}

正確寫法:用正常邏輯

public static > boolean contains(Class clz, String name) {
    for (T t : clz.getEnumConstants()) {
        if (t.name().equals(name)) return true;
    }
    return false;
}

能預防的異常要預防

// 反模式:依賴異常
FileInputStream fis = new FileInputStream(file); // 可能拋 FileNotFoundException

// 正確:先判斷
if (file.exists() && !file.isDirectory()) {
    try { FileInputStream fis = new FileInputStream(file); }
    catch (FileNotFoundException e) { ... } // 理論不會發生,作為雙重保險
}

建議 117:多使用異常,性能不是拒絕理由

性能事實

  • 創建異常對象比 String 慢 ~5 倍(需捕獲棧快照 fillInStackTrace)
  • 異常不可緩存(棧幀隨上下文變化)
  • 但 JDK 1.6+ 創建異常僅需 ~1.4ms,典型事務 100ms 內,佔比 < 1.4%

異常的價值遠超性能成本

  1. 關注點分離:正常邏輯與錯誤處理解耦
  2. 豐富信息:棧跡蹤精準定位問題
  3. 類型安全:編譯期強制處理受檢異常
  4. 面向對象本質:異常即對象,支持繼承、多態、封裝

用例文檔映射

用例:用戶登錄
主流程:輸入帳號密碼 → 驗證成功 → 進入系統
異常流程:
  E1: 帳號不存在 → 提示 "帳號錯誤"
  E2: 密碼錯誤 → 提示 "密碼錯誤"
  E3: 連續 3 次失敗 → 鎖定帳號 30 分鐘

// 代碼直接映射
public void login() {
    try { /* 正常驗證 */ }
    catch (InvalidAccountException e) { /* E1 */ }
    catch (InvalidPasswordException e) { /* E2 */ }
    catch (TooManyAttemptsException e) { /* E3 */ }
}

總結:異常處理鐵律

鐵律核心內容
構造器不拋異常改用工廠/構建器/初始化方法
善用棧跡蹤實現調用者感知、安全防護、動態行為
異常只做異常事業務判斷用 if/for,預防性檢查前置
性能不是藉口毫秒級開換取架構清晰、排查高效、類型安全

異常機制是 Java 面向對象錯誤處理的基石,正確使用它,是從「寫出能跑的代碼」邁向「寫出可維護、可診斷、健壯的系統」的關鍵一步。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved