《Java核心技術(卷1)》筆記:第7章 異常、斷言和日誌

1. 異常

  1. (P 280)異常處理需要考慮的問題:

    • 用戶輸入錯誤
    • 設備錯誤
    • 物理限制
    • 程式碼錯誤
  2. (P 280)傳統的處理錯誤的方法是:返回一個特殊的錯誤碼,常見的是返回-1或者null引用

  3. (P 280)在Java中,方法出現錯誤時,它會立即退出,不返回任何值,而是拋出一個封裝了錯誤資訊的對象

  4. (P 280)Java中所有的異常都是由Throwable繼承而來,它下面又分解為兩個分支:ErrorException

    • Error:描述了Java運行時系統的內部錯誤資源耗盡錯誤(對於處理這種錯誤,你幾乎無能為力
    • Exception:又分解為兩個分支:RuntimeException(由編程錯誤導致,如果出現該異常,那一定是你的問題)和其他異常(諸如IO錯誤這類問題)
      • 派生於RuntimeException的異常:
        • 錯誤的強制類型轉換
        • 數組訪問越界
        • 訪問null指針
      • 不是派生於RuntimeException的異常:
        • 試圖超越文件末尾繼續讀取數據
        • 試圖打開一個不存在的文件
        • 試圖根據特定的字元串查找Class對象,而這個字元串表示的類並不存在
    graph TD
    Throwable[Throwable]–>Error[Error]
    Throwable[Throwable]–>Exception[Exception]

    Error[Error]–>OtherError1[…]
    Error[Error]–>OtherError2[…]
    Error[Error]–>OtherError3[…]

    Exception[Exception]–>RuntimeException[RuntimeException]
    Exception[Exception]–>IOException[IOException]
    Exception[Exception]–>OtherException[…]

    IOException[IOException]–>OtherIOException1[…]
    IOException[IOException]–>OtherIOException2[…]
    IOException[IOException]–>OtherIOException3[…]

    RuntimeException[RuntimeException]–>OtherRuntimeException1[…]
    RuntimeException[RuntimeException]–>OtherRuntimeException2[…]
    RuntimeException[RuntimeException]–>OtherRuntimeException3[…]

  5. (P 281)派生於ErrorRuntimeException的所有異常稱為非檢查型異常,所有其他異常稱為檢查型異常

  6. (P 282)如果沒有處理器捕獲異常對象,那麼當前執行的執行緒就會終止

  7. (P 283)必須在方法的首部列出所有檢查型異常類型,但是不需要聲明從Error繼承的異常,也不應該聲明從RuntimeException繼承的那些非檢查型異常

  8. (P 283)如果在子類中覆蓋了超類的一個方法,子類方法中聲明的檢查型異常不能比超類方法中聲明的異常更通用(子類方法可以拋出更特定的異常,或者根本不拋出任何異常)。如果超類方法沒有拋出任何檢查型異常,子類也不能拋出任何檢查型異常

  9. (P 288)同一個catch子句中可以捕獲多個異常類型,如果一些異常的處理邏輯是一樣的,就可以合併catch子句。只有當捕獲的異常類型彼此之間不存在子類關係時才需要這個特性

    try {
        ...
    } catch (FileNotFoundException | UnknownHostException e) {
        ...
    } catch (IOException e) {
        ...
    }
    
  10. (P 289)可以在catch子句中拋出一個異常,此時,可以把原始異常設置為新異常的「原因」

    try {
        ...
    } catch (SQLException original) {
        var e = new ServletException("database error");
        e.initCause(original);
        throw e;
    }
    

    捕獲異常時,獲取原始異常

    Throwable original = caughtException.getCause();
    
  11. (P 292)一種推薦的異常捕獲寫法:內層try語句塊只有一個職責,就是確保釋放資源外層try語句塊也只有一個職責,就是確保報告出現的錯誤

    try {
        try {
            ...
        } finally {
            // 釋放資源
        }
    } catch (Exception e) {
        // 報告錯誤
    }
    
  12. (P 293)Java 7中,對於實現了AutoCloseable介面的類,可以使用帶資源的try語句(try-with-resources):

    try (Resources res = ...) {
        // Work with res
        ...
    }
    

    Java 9中,可以在try首部中提供之前聲明的事實最終變數(effectively final variable):

    try (res) {
        // Work with res
        ...
    } // res.close() called here
    
  13. (P 294)在try-with-resources語句中,如果try塊拋出一個異常,而且close方法也拋出一個異常,則原來的異常會重新拋出,而close方法拋出的異常會「被抑制」(可以通過getSuppressed方法得到這些被抑制的異常)

  14. (P 294)可以通過StackWalker類處理堆棧軌跡

    var walker = StackWalker.getInstance();
    walker.forEach(frame -> ...); // 例如:walker.forEach(System.out::println);
    
  15. (P 298)使用異常的一些技巧:

    • 異常處理不能代替簡單的測試(捕獲處理異常的成本很高,只在異常情況下使用異常
    • 不要過分的細化異常(有必要將整個任務包在一個try語句塊中,將正常處理與錯誤處理分開
    • 充分利用異常層次結構
    • 不要壓制異常(異常非常重要時,應該適當地進行處理)
    • 在檢測錯誤時,「苛刻」要比放任更好
    • 不要羞於傳遞異常(最好繼續傳遞異常,而不是自己捕獲)

2. 斷言

  1. (P 301)Java中引入了關鍵字assert,其有如下兩種形式:

    assert condition;
    assert condition : expression;
    

    這兩個語句都會計算條件,如果結果為false,則拋出一個AssertionError異常。在第二個語句中,表達式將傳入AssertionError對象的構造器,並轉換為一個消息字元串

  2. (P 301)默認情況下,斷言是禁用的,可以使用-enableassertions或者-ea選項啟用斷言:

    java -enableassertions MyApp
    

    禁用斷言可以使用-disableassertions-da

  3. (P 302)斷言只應該用於在測試階段確定程式內部錯誤的位置

3. 日誌

  1. (P 305)基本日誌的使用:

    • 生成簡單的日誌記錄

      Logger.getGlobal().info("hello world!");
      
    • 取消所有日誌

      Logger.getGlobal().setLevel(Level.OFF);
      
  2. (P 305)高級日誌的使用:

    • 創建或獲取日誌記錄器

      private static final Logger myLogger = Logger.getLogger("className"); // className是全限定類名
      
    • 設置日誌級別

      logger.setLevel(Level.FINE); // FINE以及更高級別的日誌都會被記錄
      
    • 記錄日誌

      // 調用相應級別的日誌記錄方法
      logger.warning(message);
      logger.fine(message);
      
      // 使用log方法並指定級別
      logger.log(Level.FINE, message);
      
      // 跟蹤執行流的方法
      logger.entering("className", "methodName", new Object[]{ params... });
      logger.exiting("className", "methodName", result);
      
      // 在日誌記錄中包含異常的描述
      logger.throwing("className", "methodName", exception);
      logger.log(Level.WARNING, message, exception);
      
  3. (P 305)7個日誌級別:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST
  4. (P 307)可以通過配置文件修改日誌系統的各個屬性,默認情況下,配置文件位於:

    conf/logging.properties
    

    指定特定位置的配置文件:

    java -Djava.util.logging.config.file=configFile MainClass
    

    指定日誌記錄器的日誌級別:在日誌記錄器名後面追加後綴.level,例如

    com.mycompany.myapp.level=FINE
    
  5. (P 313)日誌技巧

    • 對一個簡單的應用,選擇一個日誌記錄器,可以把日誌記錄器命名為與主應用包一樣的名字
    • 默認的日誌配置會把級別等於或高於INFO的所有消息記錄到控制台,用戶可以覆蓋這個默認配置,最好在你的應用中安裝一個更合適的默認配置
    • 所有級別為INFO、WARNING和SEVERE的消息都將顯示到控制台上
      • 只將對程式用戶有意義的消息設置為以上這幾個級別
      • 程式設計師想要的日誌消息設定為FINE級別是一個很好的選擇
  6. (P 321)調試技巧

    • 列印或日誌記錄變數的值

    • 在每一個類中放置一個單獨的main方法,以便獨立地測試類

    • 使用JUnit

    • 日誌代理,它是一個子類的對象,可以截獲方法調用,記錄日誌,然後調用超類中的方法

      var generator = new Random() {
          public double nextDouble() {
              double result = super.nextDouble();
              Logger.getGlobal().info("nextDouble: " + result);
              return result;
          }
      }
      
    • 利用Throwable類的printStackTrace方法,可以從任意的異常對象獲得堆棧軌跡

    • 一般來說,堆棧軌跡顯示在System.err上。如果想要記錄或顯示堆棧軌跡,可以將它捕獲到一個字元串中

      var out = new StringWriter();
      new Throwable().printStackTrace(new PrintWriter(out));
      String description = out.toString();
      
    • 通常,將程式錯誤記入一個文件會很有用:

      java MyProgram > errors.txt        # 錯誤,錯誤被發送到System.err而不是System.out
      java MyProgram 2> errors.txt       # 正確,只輸出System.err
      java MyProgram 1> errors.txt 2>&1  # 同時捕獲System.out和System.err
      
    • 將未捕獲的異常的堆棧軌跡記錄到一個文件中,而不是直接輸出到System.err,可以使用靜態方法Thread.setDefaultUncaughtExceptionHandler改變未捕獲異常的處理器

      Thread.setDefaultUncaughtExceptionHandler(
          new Thread.UncaughtExceptionHandler() {
              public void uncaughtException(Thread t, Throwable e) {
                  // save information in log file
              }
          }
      )
      
    • 要想觀察類的載入過程,啟動Java虛擬機時可以使用-verbose標誌

    • -Xlint選項告訴編譯器找出常見的程式碼問題

      javac -Xlint sourceFiles
      
    • Java虛擬機增加了對Java應用程式的監控和管理支援,允許在虛擬機中安裝代理來跟蹤記憶體消耗、執行緒使用、類載入等情況。jconsole工具可以顯示有關虛擬機性能的統計結果

    • Java任務控制器:一個專業級性能分析和診斷工具