偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Try-Catch-Finally中的4個巨坑,老程序員也搞不定!

開發(fā) 前端
在 Java 語言中 try-catch-finally 看似簡單,一副人畜無害的樣子,但想要真正的“掌控”它,卻并不是一件容易的事。

[[378885]]

本文轉載自微信公眾號「Java中文社群」,作者磊哥 。轉載本文請聯系Java中文社群公眾號。  

在 Java 語言中 try-catch-finally 看似簡單,一副人畜無害的樣子,但想要真正的“掌控”它,卻并不是一件容易的事。別的不說,咱就拿 fianlly 來說吧,別看它的功能單一,但使用起來卻“暗藏殺機”,若您不信,咱來看下面的這幾個例子...

坑1:finally中使用return

若在 finally 中使用 return,那么即使 try-catch 中有 return 操作,也不會立馬返回結果,而是再執(zhí)行完 finally 中的語句再返回。此時問題就產生了:如果 finally 中存在 return 語句,則會直接返回 finally 中的結果,從而無情的丟棄了 try 中的返回值。

① 反例代碼

  1. public static void main(String[] args) throws FileNotFoundException { 
  2.     System.out.println("執(zhí)行結果:" + test()); 
  3.  
  4. private static int test() { 
  5.     int num = 0; 
  6.     try { 
  7.         // num=1,此處不返回 
  8.         num++; 
  9.         return num; 
  10.     } catch (Exception e) { 
  11.         // do something 
  12.     } finally { 
  13.         // num=2,返回此值 
  14.         num++; 
  15.         return num; 
  16.     } 

以上代碼的執(zhí)行結果如下:

② 原因分析

如果在 finally 中存在 return 語句,那么 try-catch 中的 return 值都會被覆蓋,如果程序員在寫代碼的時候沒有發(fā)現這個問題,那么就會導致程序的執(zhí)行結果出錯。

③ 解決方案

如果 try-catch-finally 中存在 return 返回值的情況,一定要確保 return 語句只在方法的尾部出現一次。

④ 正例代碼

  1. public static void main(String[] args) throws FileNotFoundException { 
  2.     System.out.println("執(zhí)行結果:" + testAmend()); 
  3. private static int testAmend() { 
  4.     int num = 0; 
  5.     try { 
  6.         num = 1; 
  7.     } catch (Exception e) { 
  8.         // do something 
  9.     } finally { 
  10.         // do something 
  11.     } 
  12.     // 確保 return 語句只在此處出現一次 
  13.     return num; 

坑2:finally中的代碼“不執(zhí)行

”如果說上面的示例比較簡單,那么下面這個示例會給你不同的感受,直接來看代碼。

① 反例代碼

  1. public static void main(String[] args) throws FileNotFoundException { 
  2.     System.out.println("執(zhí)行結果:" + getValue()); 
  3. private static int getValue() { 
  4.     int num = 1; 
  5.     try { 
  6.         return num; 
  7.     } finally { 
  8.         num++; 
  9.     } 

以上代碼的執(zhí)行結果如下:

② 原因分析

本以為執(zhí)行的結果會是 2,但萬萬沒想到竟然是 1,用馬大師的話來講:「我大意了啊,沒有閃」。

有人可能會問:如果把代碼換成 ++num,那么結果會不會是 2 呢?

很抱歉的告訴你,并不會,執(zhí)行的結果依然是 1。那為什么會這樣呢?想要真正的搞懂它,我們就得從這段代碼的字節(jié)碼說起了。

以上代碼最終生成的字節(jié)碼如下:

  1. // class version 52.0 (52) 
  2. // access flags 0x21 
  3. public class com/example/basic/FinallyExample { 
  4.  
  5.   // compiled from: FinallyExample.java 
  6.  
  7.   // access flags 0x1 
  8.   public <init>()V 
  9.    L0 
  10.     LINENUMBER 5 L0 
  11.     ALOAD 0 
  12.     INVOKESPECIAL java/lang/Object.<init> ()V 
  13.     RETURN 
  14.    L1 
  15.     LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0 
  16.     MAXSTACK = 1 
  17.     MAXLOCALS = 1 
  18.  
  19.   // access flags 0x9 
  20.   public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException  
  21.    L0 
  22.     LINENUMBER 13 L0 
  23.     GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 
  24.     NEW java/lang/StringBuilder 
  25.     DUP 
  26.     INVOKESPECIAL java/lang/StringBuilder.<init> ()V 
  27.     LDC "\u6267\u884c\u7ed3\u679c:" 
  28.     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; 
  29.     INVOKESTATIC com/example/basic/FinallyExample.getValue ()I 
  30.     INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; 
  31.     INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 
  32.     INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V 
  33.    L1 
  34.     LINENUMBER 14 L1 
  35.     RETURN 
  36.    L2 
  37.     LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 
  38.     MAXSTACK = 3 
  39.     MAXLOCALS = 1 
  40.  
  41.   // access flags 0xA 
  42.   private static getValue()I 
  43.     TRYCATCHBLOCK L0 L1 L2 null 
  44.    L3 
  45.     LINENUMBER 18 L3 
  46.     ICONST_1 
  47.     ISTORE 0 
  48.    L0 
  49.     LINENUMBER 20 L0 
  50.     ILOAD 0 
  51.     ISTORE 1 
  52.    L1 
  53.     LINENUMBER 22 L1 
  54.     IINC 0 1 
  55.    L4 
  56.     LINENUMBER 20 L4 
  57.     ILOAD 1 
  58.     IRETURN 
  59.    L2 
  60.     LINENUMBER 22 L2 
  61.    FRAME FULL [I] [java/lang/Throwable] 
  62.     ASTORE 2 
  63.     IINC 0 1 
  64.    L5 
  65.     LINENUMBER 23 L5 
  66.     ALOAD 2 
  67.     ATHROW 
  68.    L6 
  69.     LOCALVARIABLE num I L0 L6 0 
  70.     MAXSTACK = 1 
  71.     MAXLOCALS = 3 

這些字節(jié)碼的簡易版本如下圖所示:

想要讀懂這些字節(jié)碼,首先要搞懂這些字節(jié)碼所代表的含義,這些內容可以從 Oracle 的官網查詢到(英文文檔):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

磊哥在這里對這些字節(jié)碼做一個簡單的翻譯:

iconst 是將 int 類型的值壓入操作數棧。istore 是將 int 存儲到局部變量。iload 從局部變量加載 int 值。iinc 通過下標遞增局部變量。ireturn 從操作數堆棧中返回 int 類型的值。astore 將引用存儲到局部變量中。

有了這些信息之后,我們來翻譯一下上面的字節(jié)碼內容:

  1. 0 iconst_1   在操作數棧中存儲數值 1 
  2. 1 istore_0   將操作數棧中的數據存儲在局部變量的位置 0 
  3. 2 iload_0    從局部變量讀取值到操作數棧 
  4. 3 istore_1   將操作數棧中存儲 1 存儲在局部變量的位置 1 
  5. 4 iinc 0 by 1 把局部變量位置 0 的元素進行遞增(+1)操作 
  6. 7 iload_1 將局部位置 1 的值加載到操作數棧中 
  7. 8 ireturn 返回操作數棧中的 int 值 

通過以上信息也許你并不能直觀的看出此方法的內部執(zhí)行過程,沒關系磊哥給你準備了方法執(zhí)行流程圖:

通過以上圖片我們可以看出:在 finally 語句(iinc 0, 1)執(zhí)行之前,本地變量表中存儲了兩個信息,位置 0 和位置 1 都存儲了一個值為 1 的 int 值。而在執(zhí)行 finally(iinc 0, 1)之前只把位置 0 的值進行了累加,之后又將位置 1 的值(1)返回給了操作數棧,所以當執(zhí)行返回操作(ireturn)時會從操作數棧中讀到返回值為 1 的結果,因此最終的執(zhí)行是 1 而不是 2。

③ 解決方案

關于 Java 虛擬機是如何編譯 finally 語句塊的問題,有興趣的讀者可以參考《The JavaTM Virtual Machine Specification, Second Edition》中 7.13 節(jié) Compiling finally。那里詳細介紹了 Java 虛擬機是如何編譯 finally 語句塊。

實際上,Java 虛擬機會把 finally 語句塊作為 subroutine(對于這個 subroutine 不知該如何翻譯為好,干脆就不翻譯了,免得產生歧義和誤解)直接插入到 try 語句塊或者 catch 語句塊的控制轉移語句之前。但是,還有另外一個不可忽視的因素,那就是在執(zhí)行 subroutine(也就是 finally 語句塊)之前,try 或者 catch 語句塊會保留其返回值到本地變量表(Local Variable Table)中,待 subroutine 執(zhí)行完畢之后,再恢復保留的返回值到操作數棧中,然后通過 return 或者 throw 語句將其返回給該方法的調用者(invoker)。

因此如果在 try-catch-finally 中如果有 return 操作,**一定要確保 return 語句只在方法的尾部出現一次!**這樣就能保證 try-catch-finally 中所有操作代碼都會生效。

④ 正例代碼

  1. private static int getValueByAmend() { 
  2.     int num = 1; 
  3.     try { 
  4.         // do something 
  5.     } catch (Exception e) { 
  6.         // do something 
  7.     } finally { 
  8.         num++; 
  9.     } 
  10.     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í)行結果如下:

從以上結果可以看出 finally 中的代碼并不是最后執(zhí)行的,而是在 catch 打印異常之前執(zhí)行的,這是為什么呢? 

② 原因分析

產生以上問題的真實原因其實并不是因為 try-catch-finally,當我們打開 e.printStackTrace 的源碼就能看出一些端倪了,源碼如下:

從上圖可以看出,當執(zhí)行 e.printStackTrace() 和 finally 輸出信息時,使用的并不是同一個對象。finally 使用的是標準輸出流:System.out,而 e.printStackTrace() 使用的卻是標準錯誤輸出流:System.err.println,它們執(zhí)行的效果等同于:

  1. public static void main(String[] args) { 
  2.     System.out.println("我是標準輸出流"); 
  3.     System.err.println("我是標準錯誤輸出流"); 

而以上代碼執(zhí)行結果的順序也是隨機的,而產生這一切的原因,我們或許可以通過標準錯誤輸出流(System.err)的注釋和說明文檔中看出:

我們簡單的對以上的注釋做一個簡單的翻譯:

“標準”錯誤輸出流。該流已經打開,并準備接受輸出數據。通常,此流對應于主機環(huán)境或用戶指定的顯示輸出或另一個輸出目標。按照慣例,即使主要輸出流(out 輸出流)已重定向到文件或其他目標位置,該輸出流(err 輸出流)也能用于顯示錯誤消息或其他信息,這些信息應引起用戶的立即注意。

從源碼的注釋信息可以看出,標準錯誤輸出流(System.err)和標準輸出流(System.out)使用的是不同的流對象,即使標準輸出流并定位到其他的文件,也不會影響到標準錯誤輸出流。那么我們就可以大膽的猜測:二者是獨立執(zhí)行的,并且為了更高效的輸出流信息,二者在執(zhí)行時是并行執(zhí)行的,因此我們看到的結果是打印順序總是隨機的。

為了驗證此觀點,我們將標準輸出流重定向到某個文件,然后再來觀察 System.err 能不能正常打印,實現代碼如下:

  1. public static void main(String[] args) throws FileNotFoundException { 
  2.     // 將標準輸出流的信息定位到 log.txt 中 
  3.     System.setOut(new PrintStream(new FileOutputStream("log.txt"))); 
  4.     System.out.println("我是標準輸出流"); 
  5.     System.err.println("我是標準錯誤輸出流"); 

以上代碼的執(zhí)行結果如下:

當程序執(zhí)行完成之后,我們發(fā)現在項目的根目錄出現了一個新的 log.txt 文件,打開此文件看到如下結果:

從以上結果可以看出標準輸出流和標準錯誤輸出流是彼此獨立執(zhí)行的,且 JVM 為了高效的執(zhí)行會讓二者并行運行,所以最終我們看到的結果是 finally 在 catch 之前執(zhí)行了。

③ 解決方案

知道了原因,那么問題就好處理,我們只需要將 try-catch-finally 中的輸出對象,改為統(tǒng)一的輸出流對象就可以解決此問題了。

④ 正例代碼

  1. private static void execErr() { 
  2.     try { 
  3.         throw new RuntimeException(); 
  4.     } catch (RuntimeException e) { 
  5.         System.out.println(e); 
  6.     } finally { 
  7.         System.out.println("執(zhí)行 finally."); 
  8.     } 

改成了統(tǒng)一的輸出流對象之后,我手工執(zhí)行了 n 次,并沒有發(fā)現任何問題。

坑4:finally中的代碼“不執(zhí)行”f

inally 中的代碼一定會執(zhí)行嗎?如果是之前我會毫不猶豫的說“是的”,但在遭受了社會的毒打之后,我可能會這樣回答:正常情況下 finally 中的代碼一定會執(zhí)行的,但如果遇到特殊情況 finally 中的代碼就不一定會執(zhí)行了,比如下面這些情況:

  • 在 try-catch 語句中執(zhí)行了 System.exit;
  • 在 try-catch 語句中出現了死循環(huán);
  • 在 finally 執(zhí)行之前掉電或者 JVM 崩潰了。

如果發(fā)生了以上任意一種情況,finally 中的代碼就不會執(zhí)行了。雖然感覺這一條有點“抬杠”的嫌疑,但墨菲定律告訴我們,如果一件事有可能會發(fā)生,那么他就一定會發(fā)生。所以從嚴謹的角度來說,這個觀點還是成立的,尤其是對于新手來說,神不知鬼不覺的寫出一個自己發(fā)現不了的死循環(huán)是一件很容易的事,不是嘛?

① 反例代碼

  1. public static void main(String[] args) { 
  2.     noFinally(); 
  3. private static void noFinally() { 
  4.     try { 
  5.         System.out.println("我是 try~"); 
  6.         System.exit(0); 
  7.     } catch (Exception e) { 
  8.         // do something 
  9.     } finally { 
  10.         System.out.println("我是 fially~"); 
  11.     } 

以上代碼的執(zhí)行結果如下:

 

從以上結果可以看出 finally 中的代碼并沒有執(zhí)行。

② 解決方案

排除掉代碼中的 System.exit 代碼,除非是業(yè)務需要,但也要注意如果在 try-cacth 中出現了 System.exit 的代碼,那么 finally 中的代碼將不會被執(zhí)行。

總結

本文我們展示了 finally 中存在的一些問題,有很實用的干貨,也有一些看似“杠精”的示例,但這些都從側面印證了一件事,那就是想完全掌握的 try-catch-finally 并不是一件簡單的事。最后,在強調一點,如果 try-catch-finally 中存在 return 返回值的操作,那么一定要確保 return 語句只在方法的尾部出現一次!

參考 & 鳴謝

阿里巴巴《Java開發(fā)手冊》

developer.ibm.com/zh/articles/j-lo-finally

責任編輯:武曉燕 來源: Java中文社群
相關推薦

2021-03-31 11:52:24

try-catch-fJava代碼

2024-05-10 11:43:23

C#編程

2021-01-13 09:55:29

try-catch-fJava代碼

2019-10-21 08:16:17

邊緣計算數據安全網絡安全

2014-07-31 13:41:36

程序員

2015-11-12 10:23:26

老程序員編程策略

2015-08-27 08:43:07

程序員保值

2014-03-27 11:10:46

程序員老程序員

2023-01-06 07:37:08

JavaScript技巧t性能

2018-02-06 08:36:02

簡歷程序員面試

2014-09-23 10:12:38

程序員

2019-03-25 07:14:57

程序員工程師職業(yè)

2020-09-15 06:13:05

Vue.jsJavaScript框架

2022-07-15 08:20:54

Java基礎知識

2020-06-28 09:08:08

Java語法塊開發(fā)

2015-09-16 09:57:41

swoolePHP程序員

2019-11-28 10:53:19

程序員技能開發(fā)者

2018-09-06 13:06:46

程序員焦慮谷歌

2009-04-17 15:48:41

程序員價值

2015-10-29 13:13:39

.NET程序員開發(fā)工具
點贊
收藏

51CTO技術棧公眾號