您好,登錄后才能下訂單哦!
這篇文章主要講解了“Try-Catch-Finally中的坑有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Try-Catch-Finally中的坑有哪些”吧!
坑1:finally中使用return
若在 finally 中使用 return,那么即使 try-catch 中有 return 操作,也不會(huì)立馬返回結(jié)果,而是再執(zhí)行完 finally 中的語(yǔ)句再返回。此時(shí)問(wèn)題就產(chǎn)生了:如果 finally 中存在 return 語(yǔ)句,則會(huì)直接返回 finally 中的結(jié)果,從而無(wú)情的丟棄了 try 中的返回值。
① 反例代碼
public static void main(String[] args) throws FileNotFoundException { System.out.println("執(zhí)行結(jié)果:" + test()); } private static int test() { int num = 0; try { // num=1,此處不返回 num++; return num; } catch (Exception e) { // do something } finally { // num=2,返回此值 num++; return num; } }
以上代碼的執(zhí)行結(jié)果如下:
② 原因分析
如果在 finally 中存在 return 語(yǔ)句,那么 try-catch 中的 return 值都會(huì)被覆蓋,如果程序員在寫代碼的時(shí)候沒有發(fā)現(xiàn)這個(gè)問(wèn)題,那么就會(huì)導(dǎo)致程序的執(zhí)行結(jié)果出錯(cuò)。
③ 解決方案
如果 try-catch-finally 中存在 return 返回值的情況,一定要確保 return 語(yǔ)句只在方法的尾部出現(xiàn)一次。
④ 正例代碼
public static void main(String[] args) throws FileNotFoundException { System.out.println("執(zhí)行結(jié)果:" + testAmend()); } private static int testAmend() { int num = 0; try { num = 1; } catch (Exception e) { // do something } finally { // do something } // 確保 return 語(yǔ)句只在此處出現(xiàn)一次 return num; }
坑2:finally中的代碼“不執(zhí)行
”如果說(shuō)上面的示例比較簡(jiǎn)單,那么下面這個(gè)示例會(huì)給你不同的感受,直接來(lái)看代碼。
① 反例代碼
public static void main(String[] args) throws FileNotFoundException { System.out.println("執(zhí)行結(jié)果:" + getValue()); } private static int getValue() { int num = 1; try { return num; } finally { num++; } }
以上代碼的執(zhí)行結(jié)果如下:
② 原因分析
本以為執(zhí)行的結(jié)果會(huì)是 2,但萬(wàn)萬(wàn)沒想到竟然是 1,用馬大師的話來(lái)講:「我大意了啊,沒有閃」。
有人可能會(huì)問(wèn):如果把代碼換成 ++num,那么結(jié)果會(huì)不會(huì)是 2 呢?
很抱歉的告訴你,并不會(huì),執(zhí)行的結(jié)果依然是 1。那為什么會(huì)這樣呢?想要真正的搞懂它,我們就得從這段代碼的字節(jié)碼說(shuō)起了。
以上代碼最終生成的字節(jié)碼如下:
// class version 52.0 (52) // access flags 0x21 public class com/example/basic/FinallyExample { // compiled from: FinallyExample.java // access flags 0x1 public <init>()V L0 LINENUMBER 5 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x9 public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException L0 LINENUMBER 13 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "\u6267\u884c\u7ed3\u679c:" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKESTATIC com/example/basic/FinallyExample.getValue ()I INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L1 LINENUMBER 14 L1 RETURN L2 LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 MAXSTACK = 3 MAXLOCALS = 1 // access flags 0xA private static getValue()I TRYCATCHBLOCK L0 L1 L2 null L3 LINENUMBER 18 L3 ICONST_1 ISTORE 0 L0 LINENUMBER 20 L0 ILOAD 0 ISTORE 1 L1 LINENUMBER 22 L1 IINC 0 1 L4 LINENUMBER 20 L4 ILOAD 1 IRETURN L2 LINENUMBER 22 L2 FRAME FULL [I] [java/lang/Throwable] ASTORE 2 IINC 0 1 L5 LINENUMBER 23 L5 ALOAD 2 ATHROW L6 LOCALVARIABLE num I L0 L6 0 MAXSTACK = 1 MAXLOCALS = 3 }
這些字節(jié)碼的簡(jiǎn)易版本如下圖所示:
想要讀懂這些字節(jié)碼,首先要搞懂這些字節(jié)碼所代表的含義,這些內(nèi)容可以從 Oracle 的官網(wǎng)查詢到(英文文檔):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
磊哥在這里對(duì)這些字節(jié)碼做一個(gè)簡(jiǎn)單的翻譯:
iconst 是將 int 類型的值壓入操作數(shù)棧。istore 是將 int 存儲(chǔ)到局部變量。iload 從局部變量加載 int 值。iinc 通過(guò)下標(biāo)遞增局部變量。ireturn 從操作數(shù)堆棧中返回 int 類型的值。astore 將引用存儲(chǔ)到局部變量中。
有了這些信息之后,我們來(lái)翻譯一下上面的字節(jié)碼內(nèi)容:
0 iconst_1 在操作數(shù)棧中存儲(chǔ)數(shù)值 1 1 istore_0 將操作數(shù)棧中的數(shù)據(jù)存儲(chǔ)在局部變量的位置 0 2 iload_0 從局部變量讀取值到操作數(shù)棧 3 istore_1 將操作數(shù)棧中存儲(chǔ) 1 存儲(chǔ)在局部變量的位置 1 4 iinc 0 by 1 把局部變量位置 0 的元素進(jìn)行遞增(+1)操作 7 iload_1 將局部位置 1 的值加載到操作數(shù)棧中 8 ireturn 返回操作數(shù)棧中的 int 值
通過(guò)以上信息也許你并不能直觀的看出此方法的內(nèi)部執(zhí)行過(guò)程,沒關(guān)系磊哥給你準(zhǔn)備了方法執(zhí)行流程圖:
通過(guò)以上圖片我們可以看出:在 finally 語(yǔ)句(iinc 0, 1)執(zhí)行之前,本地變量表中存儲(chǔ)了兩個(gè)信息,位置 0 和位置 1 都存儲(chǔ)了一個(gè)值為 1 的 int 值。而在執(zhí)行 finally(iinc 0, 1)之前只把位置 0 的值進(jìn)行了累加,之后又將位置 1 的值(1)返回給了操作數(shù)棧,所以當(dāng)執(zhí)行返回操作(ireturn)時(shí)會(huì)從操作數(shù)棧中讀到返回值為 1 的結(jié)果,因此最終的執(zhí)行是 1 而不是 2。
③ 解決方案
關(guān)于 Java 虛擬機(jī)是如何編譯 finally 語(yǔ)句塊的問(wèn)題,有興趣的讀者可以參考《The JavaTM Virtual Machine Specification, Second Edition》中 7.13 節(jié) Compiling finally。那里詳細(xì)介紹了 Java 虛擬機(jī)是如何編譯 finally 語(yǔ)句塊。
實(shí)際上,Java 虛擬機(jī)會(huì)把 finally 語(yǔ)句塊作為 subroutine(對(duì)于這個(gè) subroutine 不知該如何翻譯為好,干脆就不翻譯了,免得產(chǎn)生歧義和誤解)直接插入到 try 語(yǔ)句塊或者 catch 語(yǔ)句塊的控制轉(zhuǎn)移語(yǔ)句之前。但是,還有另外一個(gè)不可忽視的因素,那就是在執(zhí)行 subroutine(也就是 finally 語(yǔ)句塊)之前,try 或者 catch 語(yǔ)句塊會(huì)保留其返回值到本地變量表(Local Variable Table)中,待 subroutine 執(zhí)行完畢之后,再恢復(fù)保留的返回值到操作數(shù)棧中,然后通過(guò) return 或者 throw 語(yǔ)句將其返回給該方法的調(diào)用者(invoker)。
因此如果在 try-catch-finally 中如果有 return 操作,**一定要確保 return 語(yǔ)句只在方法的尾部出現(xiàn)一次!**這樣就能保證 try-catch-finally 中所有操作代碼都會(huì)生效。
④ 正例代碼
private static int getValueByAmend() { int num = 1; try { // do something } catch (Exception e) { // do something } finally { num++; } return num; }
坑3:finally中的代碼“非最后”執(zhí)行
① 反例代碼
public static void main(String[] args) throws FileNotFoundException { execErr();}private static void execErr() { try { throw new RuntimeException(); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("執(zhí)行 finally."); }}
以上代碼的執(zhí)行結(jié)果如下:
從以上結(jié)果可以看出 finally 中的代碼并不是最后執(zhí)行的,而是在 catch 打印異常之前執(zhí)行的,這是為什么呢?
② 原因分析
產(chǎn)生以上問(wèn)題的真實(shí)原因其實(shí)并不是因?yàn)?try-catch-finally,當(dāng)我們打開 e.printStackTrace 的源碼就能看出一些端倪了,源碼如下:
從上圖可以看出,當(dāng)執(zhí)行 e.printStackTrace() 和 finally 輸出信息時(shí),使用的并不是同一個(gè)對(duì)象。finally 使用的是標(biāo)準(zhǔn)輸出流:System.out,而 e.printStackTrace() 使用的卻是標(biāo)準(zhǔn)錯(cuò)誤輸出流:System.err.println,它們執(zhí)行的效果等同于:
public static void main(String[] args) { System.out.println("我是標(biāo)準(zhǔn)輸出流"); System.err.println("我是標(biāo)準(zhǔn)錯(cuò)誤輸出流"); }
而以上代碼執(zhí)行結(jié)果的順序也是隨機(jī)的,而產(chǎn)生這一切的原因,我們或許可以通過(guò)標(biāo)準(zhǔn)錯(cuò)誤輸出流(System.err)的注釋和說(shuō)明文檔中看出:
我們簡(jiǎn)單的對(duì)以上的注釋做一個(gè)簡(jiǎn)單的翻譯:
“標(biāo)準(zhǔn)”錯(cuò)誤輸出流。該流已經(jīng)打開,并準(zhǔn)備接受輸出數(shù)據(jù)。通常,此流對(duì)應(yīng)于主機(jī)環(huán)境或用戶指定的顯示輸出或另一個(gè)輸出目標(biāo)。按照慣例,即使主要輸出流(out 輸出流)已重定向到文件或其他目標(biāo)位置,該輸出流(err 輸出流)也能用于顯示錯(cuò)誤消息或其他信息,這些信息應(yīng)引起用戶的立即注意。
從源碼的注釋信息可以看出,標(biāo)準(zhǔn)錯(cuò)誤輸出流(System.err)和標(biāo)準(zhǔn)輸出流(System.out)使用的是不同的流對(duì)象,即使標(biāo)準(zhǔn)輸出流并定位到其他的文件,也不會(huì)影響到標(biāo)準(zhǔn)錯(cuò)誤輸出流。那么我們就可以大膽的猜測(cè):二者是獨(dú)立執(zhí)行的,并且為了更高效的輸出流信息,二者在執(zhí)行時(shí)是并行執(zhí)行的,因此我們看到的結(jié)果是打印順序總是隨機(jī)的。
為了驗(yàn)證此觀點(diǎn),我們將標(biāo)準(zhǔn)輸出流重定向到某個(gè)文件,然后再來(lái)觀察 System.err 能不能正常打印,實(shí)現(xiàn)代碼如下:
public static void main(String[] args) throws FileNotFoundException { // 將標(biāo)準(zhǔn)輸出流的信息定位到 log.txt 中 System.setOut(new PrintStream(new FileOutputStream("log.txt"))); System.out.println("我是標(biāo)準(zhǔn)輸出流"); System.err.println("我是標(biāo)準(zhǔn)錯(cuò)誤輸出流"); }
以上代碼的執(zhí)行結(jié)果如下:
當(dāng)程序執(zhí)行完成之后,我們發(fā)現(xiàn)在項(xiàng)目的根目錄出現(xiàn)了一個(gè)新的 log.txt 文件,打開此文件看到如下結(jié)果:
從以上結(jié)果可以看出標(biāo)準(zhǔn)輸出流和標(biāo)準(zhǔn)錯(cuò)誤輸出流是彼此獨(dú)立執(zhí)行的,且 JVM 為了高效的執(zhí)行會(huì)讓二者并行運(yùn)行,所以最終我們看到的結(jié)果是 finally 在 catch 之前執(zhí)行了。
③ 解決方案
知道了原因,那么問(wèn)題就好處理,我們只需要將 try-catch-finally 中的輸出對(duì)象,改為統(tǒng)一的輸出流對(duì)象就可以解決此問(wèn)題了。
④ 正例代碼
private static void execErr() { try { throw new RuntimeException(); } catch (RuntimeException e) { System.out.println(e); } finally { System.out.println("執(zhí)行 finally."); } }
改成了統(tǒng)一的輸出流對(duì)象之后,我手工執(zhí)行了 n 次,并沒有發(fā)現(xiàn)任何問(wèn)題。
坑4:finally中的代碼“不執(zhí)行”f
inally 中的代碼一定會(huì)執(zhí)行嗎?如果是之前我會(huì)毫不猶豫的說(shuō)“是的”,但在遭受了社會(huì)的毒打之后,我可能會(huì)這樣回答:正常情況下 finally 中的代碼一定會(huì)執(zhí)行的,但如果遇到特殊情況 finally 中的代碼就不一定會(huì)執(zhí)行了,比如下面這些情況:
在 try-catch 語(yǔ)句中執(zhí)行了 System.exit;
在 try-catch 語(yǔ)句中出現(xiàn)了死循環(huán);
在 finally 執(zhí)行之前掉電或者 JVM 崩潰了。
如果發(fā)生了以上任意一種情況,finally 中的代碼就不會(huì)執(zhí)行了。雖然感覺這一條有點(diǎn)“抬杠”的嫌疑,但墨菲定律告訴我們,如果一件事有可能會(huì)發(fā)生,那么他就一定會(huì)發(fā)生。所以從嚴(yán)謹(jǐn)?shù)慕嵌葋?lái)說(shuō),這個(gè)觀點(diǎn)還是成立的,尤其是對(duì)于新手來(lái)說(shuō),神不知鬼不覺的寫出一個(gè)自己發(fā)現(xiàn)不了的死循環(huán)是一件很容易的事,不是嘛?
① 反例代碼
public static void main(String[] args) { noFinally(); } private static void noFinally() { try { System.out.println("我是 try~"); System.exit(0); } catch (Exception e) { // do something } finally { System.out.println("我是 fially~"); } }
以上代碼的執(zhí)行結(jié)果如下:
從以上結(jié)果可以看出 finally 中的代碼并沒有執(zhí)行。
② 解決方案
排除掉代碼中的 System.exit 代碼,除非是業(yè)務(wù)需要,但也要注意如果在 try-cacth 中出現(xiàn)了 System.exit 的代碼,那么 finally 中的代碼將不會(huì)被執(zhí)行。
感謝各位的閱讀,以上就是“Try-Catch-Finally中的坑有哪些”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)Try-Catch-Finally中的坑有哪些這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。