歡迎光臨
每天分享高質量文章

你真的瞭解try{ return }finally{}中的return?

點擊上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

精品專欄

 

今天去逛論壇 時發現了一個很有趣的問題:

誰能給我我解釋一下這段程式的結果為什麼是:2.而不是:3

代碼如下:

  1. class Test {

  2.    public int aaa() {

  3.        int x = 1;

  4.        try {

  5.            return ++x;

  6.        } catch (Exception e) {

  7.        } finally {

  8.            ++x;

  9.        }

  10.        return x;

  11.    }

  12.    public static void main(String[] args) {

  13.        Test t = new Test();

  14.        int y = t.aaa();

  15.        System.out.println(y);

  16.    }

  17. }


看了問題後,得出了以下幾個問題:

  • 如果在 try 陳述句塊里使用 return 陳述句,那麼 finally 陳述句塊還會執行嗎?(果你的答案是不會執行,請務必要看下去 ^_^)

  • 如果執行,那麼是怎樣實現既執行 return 又執行 finally 的呢?(如果你的答案是不知道,請繼續看下去!!)

  • 上面的程式輸出為什麼是2?( 如果不知道,繼續看下去~~)

  • 在網上看到還有人還問“是先執行return還是先執行finally?”的 (個人覺得,如果知道finally會執行就可以得出是,先執行finally再執行return的。因為,如果先執行return,那麼整個函式都跳出了,那麼還怎麼執行finally?^_^)

剛看到這個問題後。突然發現基礎不夠扎實,居然來第一個都答不出來。。。(不知道還有木有和我也一樣也回答不出以上的問題的? 如果有請在評論里告訴我一聲,讓我知道,我並不孤獨~~)

根據已有的知識知道: return 是可以當作終止陳述句來用的,我們經常用它來跳出當前方法,並傳回一個值給呼叫方法。然後該方法就結束了,不會執行return下麵的陳述句。 finally :無論try陳述句發生了什麼,無論丟擲異常還是正常執行。finally陳述句都會執行。 那麼問題來了。。。。在try陳述句里使用return後,finally是否還會執行?finally一定會執行的說法是否還成立?如果成立,那麼先執行return還是先執行finally?

驗證 finally 陳述句是否會執行,以及 return 和 finally的執行順序

在求知欲的驅動下,我繼續進行更深的探索,果斷打開了Oracle的主頁,翻閱了java 官方教程的finally陳述句。發現了官方教程對這個特殊情況有說明:

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.

Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.

個人簡單翻譯:

當try陳述句退出時肯定會執行finally陳述句。這確保了即使發了一個意想不到的異常也會執行finally陳述句塊。但是finally的用處不僅是用來處理異常——它可以讓程式員不會因為return、continue、或者break陳述句而忽略了清理代碼。把清理代碼放在finally陳述句塊里是一個很好的做法,即便可能不會有異常發生也要這樣做。

註意,當try或者catch的代碼在運行的時候,JVM退出了。那麼finally陳述句塊就不會執行。同樣,如果執行緒在運行try或者catch的代碼時被中斷了或者被殺死了(killed),那麼finally陳述句可能也不會執行了,即使整個運用還會繼續執行。

從上面的官方說明,我們知道無論try里執行了return陳述句、break陳述句、還是continue陳述句,finally陳述句塊還會繼續執行。同時,在stackoverflow里也找到了一個答案,我們可以呼叫 System.exit()來終止它:

finally will be called. The only time finally won't be called is: if you call System.exit(), another thread interrupts current one, or if the JVM crashes first.

另外,在java的語言規範有講到,如果在try陳述句里有return陳述句,finally陳述句還是會執行。它會在把控制權轉移到該方法的呼叫者或者建構式前執行finally陳述句。也就是說,使用return陳述句把控制權轉移給其他的方法前會執行finally陳述句。

個人驗證

我們依然使用上面的代碼作為例子。首先,分別在以下三行代碼前加上斷點:

  • int x = 1;

  • return ++x;

  • ++x;

然後以debug樣式運行代碼。

剛開始時,效果如下圖:

按一下F6,我們可以發現,程式已經執行到 return ++x;,但還沒執行該陳述句,此刻x=1

繼續按一下F6,程式執行到 ++x;,但還沒執行該陳述句,因此此時的x=2(剛執行完return ++x陳述句的++x,但沒執行return)

繼續按一下F6,此時,我們發現程式又跳回到 return +xx 這一行,此刻x=3(執行了finally陳述句里的++x)

從上面過程中可以看到,

  • 在 try 里 使用 return 還是會執行finally陳述句的(我們用debug的樣式看到了程式會條件 finally陳述句里執行)

  • 執行完finally陳述句才執行 return。為什麼?從上面的圖可以合理推理出return +xx;是分開來執行的,先執行++x,再執行finally,最後才執行return跳出函式。因為程式調兩次跳到了 return +xx; 陳述句上。(其實要驗證 return ++x是分開兩部分執行的方法很簡單,把變數x變成static變數併在main函式里輸出,會發現x的值還是3,即使兩次跳到 return ++x 也只是第一次執行了加1操作,第二次只是執行了return而沒有執行++x。這裡是合理推理,後面有真憑實據~~)

看到這,我們可能會再次糾結起來了。從上面的驗證可以看出,finally陳述句執行了,而且x的值也確實加到3了,那麼為什麼y是2呢?

驗證為什麼是2不是3

翻看官方的jvm規範就會把一切的謎團解開了:

If the try clause executes a return, the compiled code does the following:

  1. Saves the return value (if any) in a local variable.

  2. Executes a jsr to the code for the finally clause.

  3. Upon return from the finally clause, returns the value saved in the local variable.

簡單翻譯下:

如果try陳述句里有return,那麼代碼的行為如下: 1.如果有傳回值,就把傳回值儲存到區域性變數中 2.執行jsr指令跳到finally陳述句里執行 3.執行完finally陳述句後,傳回之前儲存在區域性變數表裡的值

根據上面的說明就可以輕易地解釋為什麼是2了。 當執行到return ++x;時,jvm在執行完++x後會在區域性變數表裡另外分配一個空間來儲存當前x的值。 註意,現在還沒把值傳回給y,而是繼續執行finally陳述句里的陳述句。等執行完後再把之前儲存的值(是2不是x)傳回給y。 所以就有了y是2不是3的情況。

其實這裡還有一點要註意的是,如果你在finally里也用了return陳述句,比如return +xx。那麼y會是3。因為規範規定了,當try和finally里都有return時,會忽略try的return,而使用finally的return。

查看Test.class的位元組碼我們同樣也可以很輕鬆地知道為什麼是2而不是3:

大概講講指令操作順序: iconst1: 把常數1進棧 ---> istore1: 棧頂元素出棧並把元素儲存在本地變數表的第二個位置里(下標為1的位置里) ---> iinc 1, 1 : 本地變數表的第二個元素自增1 --->iload1:第二個元素進棧 ---> istore2:棧頂元素出棧並把元素儲存在本地變數表的第2個位置里 ---> iinc 1, 1 : 本地變數表的第二個元素自增1 ---> iload_2:第二個元素進棧 (註意,此時棧頂元素為2)---> ireturn:傳回棧頂元素。

後面的指令是要在2-7行出現異常時在跳到12行的,這個例子沒出現異常,不用關註。

上面流程棧和本地變數表的情況如下圖:

總結

  • 再次發現幫助別人解決問題的好處,不僅能幫人還能完善自己

  • 位元組碼的知識還是挺實用的,有空要深入研究下

  • 再次證明官方教程和資料真的很有用

推薦四十多條純乾貨 Java 代碼優化建議

一個致命的 Redis 命令,導致公司損失 400 萬!!

美團三面:一個執行緒OOM,行程里其他執行緒還能運行麽?

設計樣式六大原則,你真的懂了嗎?

6 個實體詳解如何把 if-else 代碼重構成高質量代碼

Get史上最優雅的加密方式!沒有之一!

如何 “幹掉” if...else

淺談String的intern

END

我是 Java 技術驛站,感謝有你


>>>>>> 加群交流技術 <<<<<<

赞(0)

分享創造快樂

© 2021 知識星球   网站地图