為什么不建議在 for 循環(huán)里捕捉異常?


在回答標(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è)贊支持一下永品。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末做鹰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鼎姐,更是在濱河造成了極大的恐慌钾麸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炕桨,死亡現(xiàn)場離奇詭異饭尝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)献宫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門钥平,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人姊途,你說我怎么就攤上這事涉瘾。” “怎么了捷兰?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵立叛,是天一觀的道長。 經(jīng)常有香客問我贡茅,道長秘蛇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任顶考,我火速辦了婚禮赁还,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驹沿。我一直安慰自己秽浇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布甚负。 她就那樣靜靜地躺著,像睡著了一般审残。 火紅的嫁衣襯著肌膚如雪梭域。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天搅轿,我揣著相機(jī)與錄音病涨,去河邊找鬼。 笑死璧坟,一個(gè)胖子當(dāng)著我的面吹牛既穆,可吹牛的內(nèi)容都是我干的赎懦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼幻工,長吁一口氣:“原來是場噩夢啊……” “哼励两!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起囊颅,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤当悔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后踢代,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盲憎,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年胳挎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了饼疙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慕爬,死狀恐怖窑眯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澡罚,我是刑警寧澤伸但,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站留搔,受9級(jí)特大地震影響更胖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隔显,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一却妨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧括眠,春花似錦彪标、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至当船,卻和暖如春题画,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背德频。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工苍息, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓竞思,卻偏偏與公主長得像表谊,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盖喷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354