啪啪打臉!領(lǐng)導(dǎo)說:try-catch要放在循環(huán)體外!
來源:Java中文社群? ? ?
今天給大家?guī)淼氖顷P(guān)于 try-catch 應(yīng)該放在循環(huán)體外,還是放在循環(huán)體內(nèi)的文章,我們將從性能和業(yè)務(wù)場景分析這兩個方面來回答此問題。
很多人對 try-catch?有一定的誤解,比如我們經(jīng)常會把它(try-catch)和“低性能”直接畫上等號,但對 try-catch 的本質(zhì)(是什么)卻缺少著最基礎(chǔ)的了解,因此我們也會在本篇中對 try-catch 的本質(zhì)進(jìn)行相關(guān)的探索。

小貼士:我會盡量用代碼和評測結(jié)果來證明問題,但由于本身認(rèn)知的局限,如有不當(dāng)之處,請讀者朋友們在評論區(qū)指出。
性能評測
話不多說,我們直接來開始今天的測試,本文我們依舊使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基準(zhǔn)測試套件)來進(jìn)行測試。
首先在 pom.xml 文件中添加 JMH 框架,配置如下:
<dependency>
???<groupId>org.openjdk.jmhgroupId>
???<artifactId>jmh-coreartifactId>
???<version>{version}version>
dependency>
完整測試代碼如下:
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)?//?預(yù)熱?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;?//?循環(huán)次數(shù)
????public?static?void?main(String[]?args)?throws?RunnerException?{
????????//?啟動基準(zhǔn)測試
????????Options?opt?=?new?OptionsBuilder()
????????????????.include(TryCatchPerformanceTest.class.getSimpleName())?//?要導(dǎo)入的測試類
????????????????.build();
????????new?Runner(opt).run();?//?執(zhí)行測試
????}
????@Benchmark
????public?int?innerForeach()?{
????????int?count?=?0;
????????for?(int?i?=?0;?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?????????????????if?(i?==?forSize)?{
????????????????????throw?new?Exception("new?Exception");
????????????????}
????????????????count++;
????????????}
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????????return?count;
????}
}
以上代碼的測試結(jié)果為:

從以上結(jié)果可以看出,程序在循環(huán) 1000 次的情況下,單次平均執(zhí)行時間為:
循環(huán)內(nèi)包含 try-catch 的平均執(zhí)行時間是 635 納秒 ±75 納秒,也就是 635 納秒上下誤差是 75 納秒; 循環(huán)外包含 try-catch 的平均執(zhí)行時間是 630 納秒,上下誤差 38 納秒。
也就是說,在沒有發(fā)生異常的情況下,除去誤差值,我們得到的結(jié)論是:try-catch 無論是在 for?循環(huán)內(nèi)還是 ?for?循環(huán)外,它們的性能相同,幾乎沒有任何差別。

try-catch的本質(zhì)
要理解 try-catch 的性能問題,必須從它的字節(jié)碼開始分析,只有這樣我能才能知道 try-catch 的本質(zhì)到底是什么,以及它是如何執(zhí)行的。
此時我們寫一個最簡單的 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?生成字節(jié)碼之后,再使用 javap -c AppTest?的命令來查看字節(jié)碼文件:
??javap?-c?AppTest?
警告:?二進(jìn)制文件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
}
從以上字節(jié)碼中可以看到有一個異常表:
Exception?table:
???????from????to??target?type
??????????0????12????12???Class?java/lang/Exception
參數(shù)說明:
from:表示 try-catch 的開始地址; to:表示 try-catch 的結(jié)束地址; target:表示異常的處理起始位; type:表示異常類名稱。
從字節(jié)碼指令可以看出,當(dāng)代碼運行時出錯時,會先判斷出錯數(shù)據(jù)是否在 from?到 to?的范圍內(nèi),如果是則從 target?標(biāo)志位往下執(zhí)行,如果沒有出錯,直接 goto?到 return。也就是說,如果代碼不出錯的話,性能幾乎是不受影響的,和正常的代碼的執(zhí)行邏輯是一樣的。

業(yè)務(wù)情況分析
雖然 try-catch 在循環(huán)體內(nèi)還是循環(huán)體外的性能是類似的,但是它們所代碼的業(yè)務(wù)含義卻完全不同,例如以下代碼:
public?class?AppTest?{
????public?static?void?main(String[]?args)?{
????????System.out.println("循環(huán)內(nèi)的執(zhí)行結(jié)果:"?+?innerForeach());
????????System.out.println("循環(huán)外的執(zhí)行結(jié)果:"?+?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;
????}
}
以上程序的執(zhí)行結(jié)果為:
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)
循環(huán)內(nèi)的執(zhí)行結(jié)果:5
循環(huán)外的執(zhí)行結(jié)果:3
可以看出在循環(huán)體內(nèi)的 try-catch 在發(fā)生異常之后,可以繼續(xù)執(zhí)行循環(huán);而循環(huán)外的 try-catch 在發(fā)生異常之后會終止循環(huán)。
因此我們在決定?try-catch 究竟是應(yīng)該放在循環(huán)內(nèi)還是循環(huán)外,不取決于性能(因為性能幾乎相同),而是應(yīng)該取決于具體的業(yè)務(wù)場景。
例如我們需要處理一批數(shù)據(jù),而無論這組數(shù)據(jù)中有哪一個數(shù)據(jù)有問題,都不能影響其他組的正常執(zhí)行,此時我們可以把 try-catch 放置在循環(huán)體內(nèi);而當(dāng)我們需要計算一組數(shù)據(jù)的合計值時,只要有一組數(shù)據(jù)有誤,我們就需要終止執(zhí)行,并拋出異常,此時我們需要將 try-catch 放置在循環(huán)體外來執(zhí)行。

總結(jié)
本文我們測試了 try-catch 放在循環(huán)體內(nèi)和循環(huán)體外的性能,發(fā)現(xiàn)二者在循環(huán)很多次的情況下性能幾乎是一致的。然后我們通過字節(jié)碼分析,發(fā)現(xiàn)只有當(dāng)發(fā)生異常時,才會對比異常表進(jìn)行異常處理,而正常情況下則可以忽略 try-catch 的執(zhí)行。但在循環(huán)體內(nèi)還是循環(huán)體外使用 try-catch,對于程序的執(zhí)行結(jié)果來說是完全不同的,因此我們應(yīng)該從實際的業(yè)務(wù)出發(fā),來決定到 try-catch 應(yīng)該存放的位置,而非性能考慮。
推薦閱讀
騷操作:不重啟 JVM,如何替換掉已經(jīng)加載的類?
放棄Spring Boot 中的 RestTemplate,我選擇 Retrofit !
騰訊 Git 規(guī)范出爐,寫給開發(fā)者的指南!
