相信你們天天都在使用Java異常機制,也相信你們對try-catch-finally執行流程爛熟於胸。本文將介紹Java異常機制的一些細節問題,這些問題雖然很小,但對代碼性能、可讀性有着較爲重要的做用。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異常等。學習
在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
複製代碼
衆所周知,首先執行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的這一特性,在實際開發中禁止使用!
空指針異常是一個運行時異常,對於這一類異常,若是沒有明確的處理策略,那麼最佳實踐在於讓程序早點掛掉,可是不少場景下,不是開發人員沒有具體的處理策略,而是根本沒有意識到空指針異常的存在。當異常真的發生的時候,處理策略也很簡單,在存在異常的地方添加一個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)
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)
String name = personOpt
.map(Person::getName)
.orElseThrow(CommonBizException::new)
.map(Optional::get);
複製代碼
if (obj != null) {...}
try { obj.method() } catch (NullPointerException e) {...}