1. 程式人生 > >(一)異常處理機制詳解

(一)異常處理機制詳解

# 前言

本文主要是對Java異常處理機制的闡述,瞭解Java的異常機制的設計和分類,及Java異常有哪些坑,如何在自定義異常類時避免採坑。

# 異常機制分類

異常情況是指阻止當前方法或作用域繼續繼續執行的情況。在Java中異常也是物件,我們可以像建立其他物件一樣,用new在堆上建立異常物件。從上圖可以看到Throwable是所有異常型別的根類,它有兩個重要的子類:Exception和Error。

  •  Error(錯誤)

Error表示編譯時和系統錯誤(除特殊情況外我們無需關注),比如程式碼允許是JVM執行錯誤,或記憶體不足時OutOfMemoryError。

  •  Exception(異常)

Exception是可以丟擲/處理的異常。在Java類庫、使用者方法及執行時故障都可能丟擲Exception型別異常,我們程式設計師需要關注的主要是Exception。它又分為執行時異常和非執行時異常。

  執行時異常:由RuntimeException和其子類異常組成。比如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)。這些異常通常是非受檢異常,可以捕獲處理或者不處理。一般有程式邏輯引起的。執行時異常的特點是Java編譯器編譯時不會檢查它,就算有這種異常編譯也能通過,究其原因,RuntimeException代表的是程式設計錯誤。

  非執行時異常:包括RuntimeException以外的異常,型別上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及使用者自定義的Exception異常。

# try-catch-finally捕獲異常

在Java中使用try-catch或者try-catch-finally捕獲異常。

## try塊

對於有可能出現異常情況的程式碼塊Code,我們可以把它放在try塊裡。

try {
//可能發生異常的程式碼塊
}

## 異常處理程式

異常處理程式必須緊跟在try塊後,以關鍵字catch表示。當異常被丟擲時,異常處理機制將負責搜尋引數與異常型別相匹配的第一個程式,然後進入catch子程式執行。

try {
//可能發生異常的程式碼塊
} catch (Type1 id1) {
//捕獲並處理異常型別為Type1的異常
} catch (Type2 id2) {
//捕獲並處理異常型別為Type1的異常
} finally {
//無論如何都會走到的程式碼
//有如下極端情況不會走到finally程式碼塊,但一般不考慮
//比如CPU掉電、執行緒異常終止等
}

// etc...

有時也可以採用maltiple catch,具體參考以下程式碼段。

## throw和throws

我們在程式設計時,需要針對某種異常情況丟擲異常給客戶端,程式碼如下

if (s == null) {
throw new NullPointerException();
}

throws是一種“異常說明”方式,它屬於方法宣告的一部分,跟在形式引數列表之後。

void func() throws Exception1, Exception2 {}

這種異常說明的方式,可以強制函式使用者強制處理該異常情況。在定義抽象基類和介面時這種能力很重要,這樣派生類就可以處理這些預先宣告的異常。

從上面可以看出throw主要是用來中斷程式執行並移交異常物件到執行時處理。throws用於宣告方法可丟擲的異常,是異常說明的一種機制。

## 使用finally做清理工作

對於一些程式碼,希望無論try塊是否有異常丟擲,都能得到執行,比如開啟的檔案控制代碼或者網路連線,可以使用try-catch-finally,程式碼如下所示。

public class FinallyWorks {
    static int count = 0;
    public static void main(String[] args) {
        while (true) {
            try {
                // count為0時拋異常
                if (count ++ == 0) {
                    throw new IOException();
                }
                System.out.println("No exception");
            } catch (Exception e) {//該句可以捕獲所有異常
                System.out.println("IOException");
            } finally {
                System.out.println("In finally clause");
                if (count == 2) break;
            }
        }
    }
}
/**
* Output
**/
IOException
In finally clause
No exception
In finally clause

## 新特性

### multiple exception

如果一個try塊中有多個異常要被捕獲,catch塊中的程式碼會變醜陋的同時還要用多餘的程式碼來記錄異常。有鑑於此,Java 7的一個新特徵是:一個catch子句中可以捕獲多個異常。示例程式碼如下:

catch(IOException | SQLException | Exception ex){
  log.warn(ex);
  throw new MyException(ex.getMessage());
}

### try-with-resources

try-with-resources[1][2] 語句會確保在try語句結束時關閉所有資源。實現了java.lang.AutoCloseable或java.io.Closeable的物件都可以做為資源。使用try-with-resources進行資源的自動關閉,在try子句中能建立一個資源物件,當程式的執行完try-catch之後,執行環境自動關閉資源。示例程式碼如下:

/**
* code 1
**/
try (FileInputStream fis = new FileInputStream("example.java")) { // line 1
     System.out.println("fis created in try-with-resources");
     doSomething(); // line2
} catch (Exception e) {
     e.printStackTrace();
}

在Java7之前我們使用finally進行資源的關閉,如下所示

/**
* code 2
**/
FileInputStream fis = new FileInputStream("example.java");
try {
     System.out.println("fis created in try-with-resources");
     doSomething(); // line 3
} catch (Exception e) {
     e.printStackTrace();
} finally {
    if (fis != null) {
        fis.close();// line 4
    }
}

異常遮蔽請參見參考文獻[1][2]

# 正確的使用異常

## 不要在finally中使用return關鍵字。

finally塊中return返回後方法結束執行,會覆蓋try塊中的return語句,換句話說就是遮蔽了try塊中的return語句。

/**
 * @author liangk
 * @date 18/09/2018
 */
public class FinallyReturn {
    public static void main(String[] args) {
        String result = finallyReturnTest();
        System.out.println(result);

    } 

    public static String finallyReturnTest() {
        try {
            System.out.println("finallyReturnTest start");
            String result = "Hello EveryBody!";
            return result;
        } finally {
            return "The finally block will be printed in the end";
        }
    }
}
/**
 * Output
 */
finallyReturnTest start
The finally block will be printed in the end

## finally 塊必須對資源物件、流物件進行關閉。

如果JKD7及以上版本,可以使用上文介紹的try-with-resources方式

## 避免直接丟擲RuntimeException及其子類。

更不允許丟擲Exception或Throwable(建議丟擲具體的異常物件)

## 建議採用預檢查方式規避RuntimeException異常,而不應該catch的方式處理。

public void readPreferences(String fileName) {
    InputStream in = new FileInputStream(fileName);
}

上面的程式如果fileName是null,就會丟擲NullPointerException,由於沒有第一時間暴露問題,堆疊資訊費解,需要相對複雜的定位。如果我們採取下面的方式,就很容易解決問題

public void readPreferences(String fileName) {
    Objects.requireNonNull(fileName);
    InputStream in = new FileInputStream(fileName);
}

## 不允許直接吞沒異常。

直接吞沒異常,既不處理也不丟擲,可能會導致難以診斷的異常情況,無法判斷異常從哪裡結束,什麼原因產生的異常情況。

## try塊只包含可能會出現異常的必要程式碼段。- try-catch 程式碼段會產生額外的效能開銷,或者換個角度說,它往往會影響JVM對程式碼進行優化,所以建議僅捕獲必要的程式碼段,不能包住整段程式碼。- Java每例項化一個Exception,都會對當時的棧進行快照,這是一個相對較重的操作。如果異常頻繁發生,開銷就無法忽略。

## 不允許使用異常實現流程控制和條件控制。

我們可以利用break\continue \if else 配合finally實現流程控制。但利用異常控制流程,比通常意義上的條件語句(if/else、switch)要低效。

## try塊放到事務程式碼中,catch異常後,如果需要回滾事務,一定注意手動回滾事務。

# 參考文獻

[1] [詳解try-with-resource](http://www.oracle.com/technetwork/cn/articles/java/trywithresources-401775-zhs.html) [2] [try-with-resource官方文件](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)