《Java核心技術(卷1)》筆記:第7章 異常、斷言和日誌
阿新 • • 發佈:2020-06-23
## 1. 異常
1. (P 280)異常處理需要考慮的問題:
* 使用者輸入錯誤
* 裝置錯誤
* 物理限制
* 程式碼錯誤
2. (P 280)傳統的處理錯誤的方法是:返回一個特殊的**錯誤碼**,常見的是返回-1或者`null`引用
3. (P 280)在Java中,方法出現錯誤時,它會立即退出,**不返回**任何值,而是丟擲一個封裝了錯誤資訊的物件
4. (P 280)Java中所有的異常都是由`Throwable`繼承而來,它下面又分解為兩個分支:`Error`和`Exception`
* `Error`:描述了Java執行時系統的**內部錯誤**和**資源耗盡**錯誤(對於處理這種錯誤,你幾乎無能為力)
* `Exception`:又分解為兩個分支:`RuntimeException`(由程式設計錯誤導致,如果出現該異常,那一定是你的問題)和其他異常(諸如IO錯誤這類問題)
* 派生於`RuntimeException`的異常:
* 錯誤的強制型別轉換
* 陣列訪問越界
* 訪問`null`指標
* 不是派生於`RuntimeException`的異常:
* 試圖超越檔案末尾繼續讀取資料
* 試圖開啟一個不存在的檔案
* 試圖根據特定的字串查詢`Class`物件,而這個字串表示的類並不存在
```mermaid
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)派生於`Error`和`RuntimeException`的所有異常稱為**非檢查型異常**,所有其他異常稱為**檢查型異常**
6. (P 282)如果沒有處理器捕獲異常物件,那麼**當前執行的執行緒**就會終止
7. (P 283)**必須**在方法的首部列出所有**檢查型異常**型別,但是不需要宣告從`Error`繼承的異常,也**不應該**宣告從`RuntimeException`繼承的那些**非檢查型異常**
8. (P 283)如果在子類中覆蓋了超類的一個方法,子類方法中宣告的檢查型異常**不能比超類方法中宣告的異常更通用**(子類方法可以丟擲更特定的異常,或者根本不丟擲任何異常)。如果超類方法**沒有**丟擲任何檢查型異常,子類也**不能丟擲任何檢查型異常**
9. (P 288)同一個`catch`子句中可以捕獲多個異常型別,如果一些異常的處理**邏輯是一樣**的,就可以**合併**`catch`子句。只有當捕獲的異常型別彼此之間**不存在子類關係**時才需要這個特性
```java
try {
...
} catch (FileNotFoundException | UnknownHostException e) {
...
} catch (IOException e) {
...
}
```
10. (P 289)可以在catch子句中丟擲一個異常,此時,可以把原始異常設定為新異常的“原因”
```java
try {
...
} catch (SQLException original) {
var e = new ServletException("database error");
e.initCause(original);
throw e;
}
```
捕獲異常時,獲取原始異常
```java
Throwable original = caughtException.getCause();
```
11. (P 292)一種**推薦**的異常捕獲寫法:**內層`try`語句塊**只有一個職責,就是**確保釋放資源**,**外層`try`語句塊**也只有一個職責,就是**確保報告出現的錯誤**
```java
try {
try {
...
} finally {
// 釋放資源
}
} catch (Exception e) {
// 報告錯誤
}
```
12. (P 293)Java 7中,對於實現了`AutoCloseable`介面的類,可以使用帶資源的`try`語句(try-with-resources):
```java
try (Resources res = ...) {
// Work with res
...
}
```
Java 9中,可以在`try`首部中提供之前宣告的事實最終變數(effectively final variable):
```java
try (res) {
// Work with res
...
} // res.close() called here
```
13. (P 294)在try-with-resources語句中,如果`try`塊丟擲一個異常,而且`close`方法也丟擲一個異常,則原來的異常會重新丟擲,而`close`方法丟擲的異常會“被抑制”(可以通過`getSuppressed`方法得到這些被抑制的異常)
14. (P 294)可以通過`StackWalker`類處理堆疊軌跡
```java
var walker = StackWalker.getInstance();
walker.forEach(frame -> ...); // 例如:walker.forEach(System.out::println);
```
15. (P 298)使用異常的一些技巧:
* 異常處理不能代替簡單的測試(捕獲處理異常的成本很高,**只在異常情況下使用異常**)
* 不要過分的細化異常(有必要將整個任務包在一個`try`語句塊中,**將正常處理與錯誤處理分開**)
* 充分利用異常層次結構
* 不要壓制異常(異常非常重要時,應該適當地進行處理)
* 在檢測錯誤時,“苛刻”要比放任更好
* 不要羞於傳遞異常(最好**繼續傳遞異常**,而不是自己捕獲)
## 2. 斷言
1. (P 301)Java中引入了關鍵字`assert`,其有如下兩種形式:
```java
assert condition;
assert condition : expression;
```
這兩個語句都會計算條件,如果結果為`false`,則丟擲一個`AssertionError`異常。在第二個語句中,表示式將傳入`AssertionError`物件的構造器,並轉換為一個訊息字串
2. (P 301)預設情況下,斷言是禁用的,可以使用`-enableassertions`或者`-ea`選項啟用斷言:
```shell
java -enableassertions MyApp
```
禁用斷言可以使用`-disableassertions`或`-da`
3. (P 302)斷言**只應該用**於在測試階段確定程式內部錯誤的位置
## 3. 日誌
1. (P 305)基本日誌的使用:
* 生成簡單的日誌記錄
```java
Logger.getGlobal().info("hello world!");
```
* 取消所有日誌
```java
Logger.getGlobal().setLevel(Level.OFF);
```
2. (P 305)高階日誌的使用:
* 建立或獲取日誌記錄器
```java
private static final Logger myLogger = Logger.getLogger("className"); // className是全限定類名
```
* 設定日誌級別
```java
logger.setLevel(Level.FINE); // FINE以及更高級別的日誌都會被記錄
```
* 記錄日誌
```java
// 呼叫相應級別的日誌記錄方法
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)可以通過配置檔案修改日誌系統的各個屬性,預設情況下,配置檔案位於:
```shell
conf/logging.properties
```
指定特定位置的配置檔案:
```shell
java -Djava.util.logging.config.file=configFile MainClass
```
指定日誌記錄器的日誌級別:在日誌記錄器名後面追加字尾.level,例如
```shell
com.mycompany.myapp.level=FINE
```
5. (P 313)日誌技巧
* 對一個簡單的應用,選擇一個日誌記錄器,可以把日誌記錄器命名為與**主應用包**一樣的名字
* 預設的日誌配置會把級別等於或高於INFO的所有訊息記錄到控制檯,使用者可以覆蓋這個預設配置,最好在你的應用中安裝一個更合適的預設配置
* 所有級別為INFO、WARNING和SEVERE的訊息都將顯示到控制檯上
* 只將對**程式使用者**有意義的訊息設定為以上這幾個級別
* 將**程式設計師**想要的日誌訊息設定為FINE級別是一個很好的選擇
6. (P 321)除錯技巧
* 列印或日誌記錄變數的值
* 在每一個類中放置一個單獨的`main`方法,以便獨立地測試類
* 使用`JUnit`
* 日誌代理,它是一個子類的物件,可以截獲方法呼叫,記錄日誌,然後呼叫超類中的方法
```java
var generator = new Random() {
public double nextDouble() {
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble: " + result);
return result;
}
}
```
* 利用`Throwable`類的`printStackTrace`方法,可以從任意的異常物件獲得堆疊軌跡
* 一般來說,堆疊軌跡顯示在`System.err`上。如果想要記錄或顯示堆疊軌跡,可以將它捕獲到一個字串中
```java
var out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();
```
* 通常,將程式錯誤記入一個檔案會很有用:
```shell
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`改變未捕獲異常的處理器
```java
Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
// save information in log file
}
}
)
```
* 要想觀察類的載入過程,啟動Java虛擬機器時可以使用`-verbose`標誌
* `-Xlint`選項告訴編譯器找出常見的程式碼問題
```shell
javac -Xlint sourceFiles
```
* Java虛擬機器增加了對Java應用程式的監控和管理支援,允許在虛擬機器中安裝代理來跟蹤記憶體消耗、執行緒使用、類載入等情況。jconsole工具可以顯示有關虛擬機器效能的統計結果
* Java任務控制器:一個專業級效能分析和診