在回答標(biāo)題這個(gè)問題之前旬痹,我們先試想一下附井,在沒有 try…catch 的情況下讨越,如果想要對(duì)函數(shù)的異常結(jié)果進(jìn)行判斷,我們應(yīng)該怎么做永毅?
異常
第一個(gè)想法肯定就是 if…else 了把跨,一般情況下,相關(guān)的代碼段我們都是放在一起的卷雕,如果此時(shí)你的程序中有大量的代碼段要做這做判斷节猿,這就意味著后面執(zhí)行的邏輯會(huì)依賴你前面語句的執(zhí)行情況,也就意味著你每調(diào)用一個(gè)可能會(huì)出現(xiàn)錯(cuò)誤的函數(shù)的時(shí)候漫雕,都要先判斷是否成功滨嘱,然后再繼續(xù)執(zhí)行后面的語句。這就會(huì)導(dǎo)致你的代碼中會(huì)充斥著大量的 if…else浸间。
Java 是一門工程性的語言太雨,而工程也是一種藝術(shù),因此采用這樣的做法顯然是很不優(yōu)雅的魁蒜∧野猓《Thinking in Java》中提到“badly formed code will not be run.”,意思是結(jié)構(gòu)不優(yōu)雅的代碼不應(yīng)該被執(zhí)行兜看,于是一個(gè)適用于 Java 的異常處理機(jī)制便應(yīng)運(yùn)而生了锥咸。
Java 的異常處理其目的在于通過使用少于目前數(shù)量的代碼來簡化大型程序,舉個(gè)簡單的例子 ??
不用 try…catch
FileReader fr = new FileReader("path");
if (fr == null) {
System.err.println("Open File Error");
} else {
BufferedReader br = new BufferedReader(fr);
while (br.ready()) {
String line = br.readLine();
if (line == null) {
System.err.println("Read Line Error");
} else {
System.out.println(line);
}
}
}
用了 try…catch
try {
FileReader fr = new FileReader("path");
BufferedReader br = new BufferedReader(fr);
while (br.ready()) {
String line = br.readLine();
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
很明顯我們可以看出來细移,下面這種寫法主線明確搏予,可讀性更高。
當(dāng)然弧轧,try…catch 也并不是百利而無一害雪侥。如果程序員在代碼中濫用了 try…catch,并且沒有做好異常處理精绎,很有可能會(huì)導(dǎo)致一些 bug 被隱藏速缨,無法跟蹤。不過這些不是本文的重點(diǎn)代乃。有興趣的可以去閱讀下《Thinking in Java》的第 12 章「通過異常處理錯(cuò)誤」旬牲。
單獨(dú)捕獲異常
在探究將異常捕獲與循環(huán)結(jié)合起來之前,我們先看一下單獨(dú)捕獲一個(gè)異常會(huì)發(fā)生什么襟己?
這是一段異常代碼
我們用 javap -c ExceptionDemo.class
來打印出他的字節(jié)碼來看一下
指令含義不是本文的重點(diǎn)引谜,所以這里就不介紹具體的含義,感興趣可以到 Oracle 官網(wǎng)查看相應(yīng)指令的含義
??The Java Virtual Machine Instruction Set
異常表的四個(gè)參數(shù)
從輸出看擎浴,字節(jié)碼分兩部分员咽,code(指令)和 exception table(異常表)兩部分。當(dāng)將 java 源碼編譯成相應(yīng)的字節(jié)碼的時(shí)候贮预,如果方法內(nèi)有 try catch 異常處理贝室,就會(huì)產(chǎn)生與該方法相關(guān)聯(lián)的異常表契讲,也就是Exception table:
部分。
每一個(gè)條目有四列信息: 異常聲明的開始行, 結(jié)束行, 異常捕獲后跳轉(zhuǎn)到的代碼計(jì)數(shù)器(PC)所指向的行數(shù), 還有一個(gè)表示捕獲的異常類的常量池索引滑频。
那這些信息是從哪來獲得的呢捡偏?這里我們先來來復(fù)習(xí)一下 JVM 的相關(guān)知識(shí):
一個(gè)線程就是一個(gè)棧,由棧幀組成峡迷,一個(gè)方法就是一個(gè)棧幀银伟,內(nèi)部保存著: 局部變量表、操作數(shù)棧绘搞、動(dòng)態(tài)鏈接彤避、方法出口。
JVM 在構(gòu)造異常實(shí)例時(shí)需要生成該異常的棧軌跡夯辖。這個(gè)操作會(huì)逐一訪問當(dāng)前線程的棧幀琉预,并且記錄下各種調(diào)試信息,包括棧幀所指向方法的名字蒿褂,方法所在的類名圆米、文件名,以及在代碼中的第幾行觸發(fā)該異常等信息啄栓。而這些信息就會(huì)存儲(chǔ)在剛才所說的Exception table:
中娄帖。
四個(gè)參數(shù)的作用
那剛才所說的那些信息又有什么用呢?
如果在執(zhí)行方法時(shí)有一個(gè)異常被拋出, JVM 就會(huì)從異常表中按照條目所出現(xiàn)的順序查找對(duì)應(yīng)的條目昙楚。如果異常拋出時(shí) PC 計(jì)數(shù)器所指向的行數(shù)正好落在異常表中某一條目包含的范圍內(nèi), 并且所拋出的異常正好是異常表中 type 列所指定的異常(或者所指定異常的子類), 那么 JVM 就會(huì)將 PC 計(jì)數(shù)器指向 Target 偏移量所指向的地址, (進(jìn)入 catch 塊)繼續(xù)執(zhí)行块茁。
如果沒有在異常表中找到異常, JVM 就會(huì)將當(dāng)前棧幀彈出并重新拋出這個(gè)異常。當(dāng) JVM 彈出當(dāng)前棧幀的時(shí)候, 它就會(huì)中止當(dāng)前方法的執(zhí)行, 返回到調(diào)用當(dāng)前方法的外部方法中, 不過并不會(huì)像正常沒有異常發(fā)生時(shí)那樣繼續(xù)執(zhí)行外部方法, 而是在外部方法中拋出相同的異常, 這樣將會(huì)導(dǎo)致 JVM 會(huì)在外部方法中重復(fù)查詢異常表并處理異常的過程桂肌。
為什么捕獲異常消耗性能
其實(shí)從上面的分析中,我們就已經(jīng)可以理解為什么捕獲異常是一個(gè)消耗性能的操作了永淌,當(dāng)你 new 一個(gè) exception 的時(shí)候崎场,JVM 已經(jīng)在 exception 里構(gòu)建好了所有的 stacktrace:
現(xiàn)在 Java 領(lǐng)域最火的框架莫過于 Spring 系列了,在一個(gè) web 項(xiàng)目中遂蛀,調(diào)用棧的深度是相當(dāng)大的谭跨,由此可見這里花費(fèi)的代價(jià)是可觀的,因此李滴,當(dāng)你對(duì) stacktrace 不感興趣的時(shí)候螃宙,不需要這樣的信息時(shí),最好不要隨便的 new exception所坯。
異常+for 循環(huán)
說了那么多其實(shí)都是前置知識(shí)谆扎,現(xiàn)在我們終于來到了標(biāo)題提到的問題了。
for 循環(huán)和異常有兩種結(jié)合方式:
try+for 循環(huán)
public static void tryFor() {
int j = 3;
try {
for (int i = 0; i < 1000; i++) {
Math.sin(j);
}
} catch (Exception e) {
e.printStackTrace();
}
}
for 循環(huán)+try
public static void forTry() {
int j = 3;
for (int i = 0; i < 1000; i++) {
try {
Math.sin(j);
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先我先給出結(jié)論:
在沒有發(fā)生異常時(shí)芹助,兩者性能上沒有差異堂湖。如果發(fā)生異常闲先,兩者的處理邏輯不一樣,雖然已經(jīng)不具有比較的意義了无蜂,但 for 循環(huán)+try 的耗時(shí)更明顯伺糠。
字節(jié)碼比較
我們對(duì)這兩種方式進(jìn)行一個(gè)字節(jié)碼的比較:
通過第二節(jié)的分析我們知道,當(dāng)程序出現(xiàn)異常時(shí)斥季,java 虛擬機(jī)就會(huì)查找方法對(duì)應(yīng)的異常表训桶,如果發(fā)現(xiàn)有聲明的異常與拋出的異常類型匹配就會(huì)跳轉(zhuǎn)到 catch 處執(zhí)行相應(yīng)的邏輯,如果沒有匹配成功酣倾,就會(huì)回到上層調(diào)用方法中繼續(xù)查找舵揭,如此反復(fù),一直到異常被處理為止灶挟,或者停止進(jìn)程琉朽。而在 for 循環(huán)中進(jìn)行 try…catch 操作,會(huì)不斷的進(jìn)行這一過程稚铣,性能損耗自然會(huì)很恐怖箱叁。
測試比較
說了這么多我們一直都是紙上談兵,口說無憑惕医,實(shí)際的效果肯定是要跑一下才知道耕漱,這里我們采用 Java 的一個(gè)微基準(zhǔn)測試框架JMH來進(jìn)行此次測試。
測試結(jié)果
Benchmark Mode Cnt Score Error Units
ExceptionDemo.forTry thrpt 20 70.236 ± 8.945 ops/ms
ExceptionDemo.tryFor thrpt 20 85.864 ± 3.272 ops/ms
score 的結(jié)果是 xxx ± xxx抬伺,單位是每毫秒多少個(gè)操作螟够。最終結(jié)果也驗(yàn)證了我們的結(jié)論。tryFor 的確會(huì)比 forTry 更節(jié)省性能峡钓。
最后
本文從異常出發(fā)妓笙,分析了單獨(dú)捕獲異常和將異常與 for 循環(huán)結(jié)合的幾種不同的情況,然后通過 JMH 進(jìn)行了一次測試能岩,最終驗(yàn)證我們標(biāo)題所說的寞宫,不建議在 for 循環(huán)里捕捉異常。
當(dāng)然拉鹃,try…catch 對(duì)性能的影響除了第二節(jié)所提到的需要維護(hù)一個(gè)異常表之外辈赋,還有一個(gè)原因,那就是 try 塊會(huì)阻止 java 的優(yōu)化(例如重排序)膏燕,try catch 里面的代碼是不會(huì)被編譯器優(yōu)化重排的钥屈。當(dāng)然重排序是需要一定的條件觸發(fā)。一般而言坝辫,只要 try 塊范圍越小篷就,對(duì) java 的優(yōu)化機(jī)制的影響是就越小。所以保證 try 塊范圍盡量只覆蓋拋出異常的地方阀溶,就可以使得異常對(duì) java 優(yōu)化的機(jī)制的影響最小化腻脏。
以上就是本文的全部內(nèi)容了鸦泳,如果你覺得有所幫助,不妨點(diǎn)個(gè)贊支持一下永品。