1. 程式人生 > >啪啪,打臉了!領導說:try-catch必須放在迴圈體外!

啪啪,打臉了!領導說:try-catch必須放在迴圈體外!

哈嘍,親愛的小夥伴們,**技術學磊哥,進步沒得說**!歡迎來到新一期的效能解讀系列,我是磊哥。 今天給大家帶來的是關於 **try-catch 應該放在迴圈體外,還是放在迴圈體內的文章**,我們將從**效能**和**業務場景分析**這兩個方面來回答此問題。 很多人對 try-catch 有一定的誤解,比如我們經常會把它(try-catch)和“低效能”直接畫上等號,但對 try-catch 的本質(是什麼)卻缺少著最基礎的瞭解,因此我們也**會在本篇中對 try-catch 的本質進行相關的探索**。 ![](https://img2020.cnblogs.com/blog/172074/202006/172074-20200602103500426-360613190.png) > 小貼士:我會盡量用程式碼和評測結果來證明問題,但由於本身認知的侷限,如有不當之處,請讀者朋友們在評論區指出。 ## 效能評測 話不多說,我們直接來開始今天的測試,本文我們依舊使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基準測試套件)來進行測試。 首先在 pom.xml 檔案中新增 JMH 框架,配置如下: ``` org.openjdk.jmh jmh-core {version} ``` 完整測試程式碼如下: ``` import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.TimeUnit; /** * try - catch 效能測試 */ @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 1 輪,每次 1s @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s @Fork(1) // fork 1 個執行緒 @State(Scope.Benchmark) @Threads(100) public class TryCatchPerformanceTest { private static final int forSize = 1000; // 迴圈次數 public static void main(String[] args) throws RunnerException { // 啟動基準測試 Options opt = new OptionsBuilder() .include(TryCatchPerformanceTest.class.getSimpleName()) // 要匯入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Benchmark public int innerForeach() { int count = 0; for (int i = 0; i < forSize; i++) { try { if (i == forSize) { throw new Exception("new Exception"); } count++; } catch (Exception e) { e.printStackTrace(); } } return count; } @Benchmark public int outerForeach() { int count = 0; try { for (int i = 0; i < forSize; i++) { if (i == forSize) { throw new Exception("new Exception"); } count++; } } catch (Exception e) { e.printStackTrace(); } return count; } } ``` 以上程式碼的測試結果為: ![](https://img2020.cnblogs.com/blog/172074/202006/172074-20200602103522797-1202469682.png) 從以上結果可以看出,程式在迴圈 1000 次的情況下,單次平均執行時間為: - 迴圈內包含 try-catch 的平均執行時間是 635 納秒 ±75 納秒,也就是 635 納秒上下誤差是 75 納秒; - 迴圈外包含 try-catch 的平均執行時間是 630 納秒,上下誤差 38 納秒。 也就是說,在沒有發生異常的情況下,除去誤差值,我們得到的結論是:**try-catch 無論是在 `for` 迴圈內還是 `for` 迴圈外,它們的效能相同,幾乎沒有任何差別**。 ![](https://img2020.cnblogs.com/blog/172074/202006/172074-20200602103536223-1814155904.png) ## try-catch的本質 要理解 try-catch 的效能問題,必須從它的位元組碼開始分析,只有這樣我能才能知道 try-catch 的本質到底是什麼,以及它是如何執行的。 此時我們寫一個最簡單的 try-catch 程式碼: ``` public class AppTest { public static void main(String[] args) { try { int count = 0; throw new Exception("new Exception"); } catch (Exception e) { e.printStackTrace(); } } } ``` 然後使用 `javac` 生成位元組碼之後,再使用 `javap -c AppTest` 的命令來檢視位元組碼檔案: ``` ➜ javap -c AppTest 警告: 二進位制檔案AppTest包含com.example.AppTest Compiled from "AppTest.java" public class com.example.AppTest { public com.example.AppTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: new #2 // class java/lang/Exception 5: dup 6: ldc #3 // String new Exception 8: invokespecial #4 // Method java/lang/Exception."":(Ljava/lang/String;)V 11: athrow 12: astore_1 13: aload_1 14: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V 17: return Exception table: from to target type 0 12 12 Class java/lang/Exception } ``` 從以上位元組碼中可以看到有一個異常表: ``` Exception table: from to target type 0 12 12 Class java/lang/Exception ``` 引數說明: - from:表示 try-catch 的開始地址; - to:表示 try-catch 的結束地址; - target:表示異常的處理起始位; - type:表示異常類名稱。 從位元組碼指令可以看出,當代碼執行時出錯時,會先判斷出錯資料是否在 `from` 到 `to` 的範圍內,如果是則從 `target` 標誌位往下執行,如果沒有出錯,直接 `goto` 到 `return`。也就是說,如果程式碼不出錯的話,效能幾乎是不受影響的,和正常的程式碼的執行邏輯是一樣的。 ![](https://img2020.cnblogs.com/blog/172074/202006/172074-20200602103550763-1577639714.png) ## 業務情況分析 雖然 try-catch 在迴圈體內還是迴圈體外的效能是類似的,但是它們所程式碼的業務含義卻完全不同,例如以下程式碼: ``` public class AppTest { public static void main(String[] args) { System.out.println("迴圈內的執行結果:" + innerForeach()); System.out.println("迴圈外的執行結果:" + outerForeach()); } // 方法一 public static int innerForeach() { int count = 0; for (int i = 0; i < 6; i++) { try { if (i == 3) { throw new Exception("new Exception"); } count++; } catch (Exception e) { e.printStackTrace(); } } return count; } // 方法二 public static int outerForeach() { int count = 0; try { for (int i = 0; i < 6; i++) { if (i == 3) { throw new Exception("new Exception"); } count++; } } catch (Exception e) { e.printStackTrace(); } return count; } } ``` 以上程式的執行結果為: > java.lang.Exception: new Exception > > at com.example.AppTest.innerForeach(AppTest.java:15) > > at com.example.AppTest.main(AppTest.java:5) > > java.lang.Exception: new Exception > > at com.example.AppTest.outerForeach(AppTest.java:31) > > at com.example.AppTest.main(AppTest.java:6) > > 迴圈內的執行結果:5 > > 迴圈外的執行結果:3 可以看出在迴圈體內的 try-catch 在發生異常之後,可以繼續執行迴圈;而迴圈外的 try-catch 在發生異常之後會終止迴圈。 因此我們**在決定 try-catch 究竟是應該放在迴圈內還是迴圈外,不取決於效能(因為效能幾乎相同),而是應該取決於具體的業務場景**。 例如我們需要處理一批資料,而無論這組資料中有哪一個資料有問題,都不能影響其他組的正常執行,此時我們可以把 try-catch 放置在迴圈體內;而當我們需要計算一組資料的合計值時,只要有一組資料有誤,我們就需要終止執行,並丟擲異常,此時我們需要將 try-catch 放置在迴圈體外來執行。 ![](https://img2020.cnblogs.com/blog/172074/202006/172074-20200602103605909-629785295.png) ## 總結 本文我們測試了 try-catch 放在迴圈體內和迴圈體外的效能,發現**二者在迴圈很多次的情況下效能幾乎是一致的**。然後我們通過位元組碼分析,發現只有當發生異常時,才會對比異常表進行異常處理,而正常情況下則可以忽略 try-catch 的執行。但在迴圈體內還是迴圈體外使用 try-catch,對於程式的執行結果來說是完全不同的,因此**我們應該從實際的業務出發,來決定到 try-catch 應該存放的位置,而非效能考慮**。 > 關注公眾號「Java中文社群」回覆“乾貨”,獲取 50 篇原創乾貨 **Top 榜**。