揭曉Java異常體系中的祕密

相信你們天天都在使用Java異常機制,也相信你們對try-catch-finally執行流程爛熟於胸。本文將介紹Java異常機制的一些細節問題,這些問題雖然很小,但對代碼性能、可讀性有着較爲重要的做用。java

1. Java異常體系介紹

在學習一項技術前,必定要先站在制高點俯瞰技術全局,從宏觀上把控某項技術的整個脈絡結構。這樣你就能夠有針對性地學習該體系結構中最重要的知識點,而且在學習細節的時候不至於鑽入牛角尖。因此,在介紹Java異常你所不知道的一些祕密以前,先讓你們複習一下Java異常體系。api

Throwable是整個Java異常體系的頂層父類,它有兩個子類,分別是:Error和Exception。app

Error表示系統致命錯誤,程序沒法處理的錯誤,好比OutOfMemoryError、ThreadDeath等。這些錯誤發生時,Java虛擬機只能終止線程。ide

Exception是程序自己能夠處理的異常,這種異常分兩大類運行時異常和非運行時異常。函數

運行時異常都是RuntimeException類及其子類異常,如NullPointerException、IndexOutOfBoundsException等,這些異常屬於unchecked異常,開發人員能夠選擇捕獲處理,也能夠不處理。這些異常通常是由程序邏輯錯誤引發的,程序應該從邏輯角度儘量避免這類異常的發生。性能

在Exception異常體系中,除了RuntimeException類及其子類的異常,均屬於checked異常。當你調用了拋出這些異常的方法後,必需要處理這些異常。若是不處理,程序就不能編譯經過。如:IOException、SQLException、用戶自定義的Exception異常等。學習

2. try-with-resources

在JDK 1.7以前,處理IO操做很是麻煩。因爲IOException屬於checked異常,調用者必須經過try-catch處理他們;又由於IO操做完成後須要關閉資源,然而關閉資源的close()方法也會拋出checked異常,所以也須要使用try-catch處理該異常。所以,本來小小的一段IO操做代碼會被複雜的try-catch嵌套包裹,從而極大地下降了程序的可讀性。spa

一個標準的IO操做代碼以下:線程

public class Demo {
    public static void main(String[] args) {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    if (bout != null) {
                        try {
                            bout.close();
                        }
                        catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
複製代碼

上述代碼使用一個輸出流bin和一個輸入六bout,將一個文件中的數據寫入另外一個文件。因爲IO資源很是寶貴,所以在完成操做後,必須在finally中分別釋放這兩個資源。而且爲了可以正確釋放這兩個IO資源,須要用兩個finally代碼塊嵌套的方式完成資源的釋放。設計

在上述40行代碼中,真正處理IO操做的代碼不到10行,而其他30行代碼都是用於保證資源合理釋放的。這顯然致使代碼可讀性較差。不過好在JDK 1.7提供了try-with-resources解決這一問題。修改後的代碼以下:

public class TryWithResource {
    public static void main(String[] args) {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

咱們須要將資源聲明代碼放入try後的括號中,而後將資源處理代碼放入try後的{}中,catch代碼塊中仍然進行異常處理,而且無需寫finally代碼塊。

那麼,try-with-resources爲何可以避免大量資源釋放代碼呢?答案是,由Java編譯器來幫咱們添加finally代碼塊。注意,編譯器只會添加finally代碼塊,而資源釋放的過程須要資源提供者提供。

在JDK 1.7中,全部的IO類都實現了AutoCloseable接口,而且須要實現其中的close()函數,資源釋放過程須要在該函數中完成。

那麼,編譯器在編譯時,會自動添加finally代碼塊,並將close()函數中的資源釋放代碼加入finally代碼塊中。從而提升代碼可讀性。

異常屏蔽問題

在try-catch-finally代碼塊中,若是try塊、catch塊和finally塊均有異常拋出,那麼最終只能拋出finally塊中的異常,而try塊和catch塊中的異常將會被屏蔽。這就是異常屏蔽問題。以下面代碼所示:

public class Connection implements AutoCloseable {
    public void sendData() throws Exception {
        throw new Exception("send data");
    }
    @Override
    public void close() throws Exception {
        throw new MyException("close");
    }
}
複製代碼

首先定義一個Connection類,該類提供了sendData()close()方法,爲了實驗須要,這兩個方法沒有任何業務邏輯,都直接拋出一個異常。下面咱們使用這個類。

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void test() throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        }
        finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}
複製代碼

當執行conn.sendData()時,該方法會將異常拋給調用者main(),但在拋以前會先執行finally塊。當執行finally塊中的conn.close()方法時,也會向調用者拋一個異常。此時,由try塊拋出的異常將會被覆蓋,main方法中僅打印finally塊中的異常。其結果以下所示:

basic.exception.MyException: close
	at basic.exception.Connection.close(Connection.java:10)
	at basic.exception.TryWithResource.test(TryWithResource.java:82)
	at basic.exception.TryWithResource.main(TryWithResource.java:7)
	......
複製代碼

這就是try-catch-finally的異常屏蔽問題,而try-with-resources能很好地解決這一問題。那麼,它是如何解決這一問題的呢?

咱們首先將這段代碼用try-with-resources改寫:

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void test() throws Exception {
        Connection conn = null;
        try (conn = new Connection();) {
            conn.sendData();
        }
    }
}
複製代碼

爲了能清楚地瞭解Java編譯器在try-with-resources上所作的事情,咱們反編譯這段代碼,獲得以下代碼:

public class TryWithResource {
    public TryWithResource() {
    }
    public static void main(String[] args) {
        try {
            // 資源聲明代碼
            Connection e = new Connection();
            Throwable var2 = null;
            try {
                // 資源使用代碼
                e.sendData();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                // 資源釋放代碼
                if(e != null) {
                    if(var2 != null) {
                        try {
                            e.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        e.close();
                    }
                }
            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }
    }
}
複製代碼

最核心的操做是22行var2.addSuppressed(var11);。編譯器將try塊和catch塊中的異常先存入一個局部變量,當finally塊中再次拋出異常時,經過以前異常的addSuppressed()方法將當前異常添加至其異常棧中,從而保證了try塊和catch塊中的異常不丟失。當使用了try-with-resources後,輸出結果以下所示:

java.lang.Exception: send data
	at basic.exception.Connection.sendData(Connection.java:5)
	at basic.exception.TryWithResource.main(TryWithResource.java:14)
	......
	Suppressed: basic.exception.MyException: close
		at basic.exception.Connection.close(Connection.java:10)
		at basic.exception.TryWithResource.main(TryWithResource.java:15)
		... 5 more
複製代碼

3. try-catch-finally執行流程

衆所周知,首先執行try中代碼,若未發生異常,則直接執行finally中代碼;若發生異常,則先執行catch中代碼後,再執行finally中代碼。

相信上述流程你們都爛熟於胸,但若是try塊和catch塊中出現了return呢?出現了throw呢?此時執行順序就會發生變化。

可是,萬變不離其中,你們只要記住一點:fianlly中的return、throw會覆蓋try、catch中的return、throw。此話怎講?請繼續往下閱讀。

要解釋這個問題,先來看一個例子,請問下面代碼中的test()函數會返回什麼結果?

public int test() {
    try {
        int a = 1;
        a = a / 0;
        return a;
    } catch (Exception e) {
        return -1;
    } finally{
        return -2;
    }
}
複製代碼

答案是-2。

當執行代碼a = a / 0;時發生異常,try塊中它以後的代碼便再也不執行,而是直接執行catch中代碼; 在catch塊中,當在執行return -1前,先會執行finally塊; 因爲finally塊中有return語句,所以catch中的return將會被覆蓋,直接執行fianlly中的return -2後程序結束。所以輸出結果是-2。

一樣地,將return換成throw也是同樣的結果,finally會覆蓋try、catch塊中的return、throw。

特別提醒:禁止在finally塊中使用return語句!這裏舉例子只是告訴你Java的這一特性,在實際開發中禁止使用!

4. Optional優雅解決NPE問題

空指針異常是一個運行時異常,對於這一類異常,若是沒有明確的處理策略,那麼最佳實踐在於讓程序早點掛掉,可是不少場景下,不是開發人員沒有具體的處理策略,而是根本沒有意識到空指針異常的存在。當異常真的發生的時候,處理策略也很簡單,在存在異常的地方添加一個if語句斷定便可,可是這樣的應對策略會讓咱們的程序出現愈來愈多的null斷定,咱們知道一個良好的程序設計,應該讓代碼中儘可能少出現null關鍵字,而java8所提供的Optional類則在減小NullPointException的同時,也提高了代碼的美觀度。但首先咱們須要明確的是,它並 不是對null關鍵字的一種替代,而是對於null斷定提供了一種更加優雅的實現,從而避免NullPointException。

假設存在以下Person類:

class Person{
    private long id;
    private String name;
    private int age;
    
    // 省略setter、getter
}
複製代碼

當咱們調用某一個接口,獲取到一個Person對象,此時能夠經過以下方法對它進行處理:

  • ofNullable(person)
    • 將Person對象轉化成Optional對象
    • 容許person爲空
Optional<Person> personOpt = Optional.ofNullable(person);
複製代碼
  • T orElse(T other)
    • 若爲空,則賦予默認值
personOpt.orElse(new Person("柴毛毛"));
複製代碼
  • T orElseGet(Supplier<? extends T> other)
    • 若爲空,則執行相應代碼,並返回默認值
personOpt.orElseGet(()->{
    Person person = new Person();
    person.setName("柴毛毛");
    person.setAge(20);
    return person;
});
複製代碼
  • <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
    • 若爲空,則拋異常
personOpt.orElseThrow(CommonBizException::new);
複製代碼
  • <U>Optional<U> map(Function<? super T,? extends U> mapper)
    • 映射(獲取person中的姓名)
String name = personOpt
                .map(Person::getName)
                .orElseThrow(CommonBizException::new)
                .map(Optional::get);
複製代碼

5. 異常處理規約

  • Java 類庫中定義的一類 RuntimeException 能夠經過預先檢查進行規避,而不該該經過 catch 來處理,好比: IndexOutOfBoundsException,NullPointerException等等。
    • 正例: if (obj != null) {...}
    • 反例: try { obj.method() } catch (NullPointerException e) {...}
  • 異常不要用來作流程控制,條件控制,由於異常的處理效率比條件分支低。
  • 對大段代碼進行 try-catch,這是不負責任的表現。catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是不管如何不會出錯的代碼。對於非穩定代碼的catch儘量進行區分異常類型,再作對應的異常處理。
  • 捕獲異常是爲了處理它,不要捕獲了卻什麼都不處理而拋棄之,若是不想處理它,請 將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化爲用戶能夠理解的內容。
  • 有 try 塊放到了事務代碼中,catch 異常後,若是須要回滾事務,必定要注意手動回滾事務。
  • 不能在 finally 塊中使用 return,finally 塊中的 return 返回後方法結束執行,不會再執行 try 塊中的 return 語句。
  • finally 塊必須對資源對象、流對象進行關閉,有異常也要作 try-catch。 說明:若是 JDK7 及以上,可使用 try-with-resources 方式。
  • 有 try 塊放到了事務代碼中,catch 異常後,若是須要回滾事務,必定要注意手動回滾事務。
  • 捕獲異常與拋異常,必須是徹底匹配,或者捕獲異常是拋異常的父類。也就是拋出的異常必須是所捕獲異常或其子類。這樣才能讓異常大而化小小而化了。
  • 本規約明確防止 NPE 是調用者的責任。即便被調用方法返回空集合或者空對象,對調用 者來講,也並不是高枕無憂,必須考慮到遠程調用失敗,運行時異常等場景返回 null 的狀況。
  • 定義時區分unchecked/checked 異常,避免直接使用RuntimeException拋出, 更不容許拋出 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義 過的自定義異常,如:DAOException / ServiceException 等。
  • 在代碼中使用「拋異常」仍是「返回錯誤碼」:
    • 對於公司外的 http/api 開放接口必須 使用「錯誤碼」;
    • 而應用內部推薦異常拋出;
    • 跨應用間 RPC 調用優先考慮使用 Result 方式,封裝 isSuccess、「錯誤碼」、「錯誤簡短信息」。

相關文章
相關標籤/搜索