三年前,我做錯(cuò)的那道面試題
三年前,我做了一道關(guān)于try-catch-finnaly的面試題,但我做錯(cuò)了,當(dāng)時(shí)面試官問(wèn)我為啥錯(cuò)了,我告訴他,我平常不會(huì)寫這么傻逼的代碼,然后面試官就沒(méi)有問(wèn)我了。。。。
最近看到其他面試的童鞋,又讓我想起了這道題,剛好也試著分析下。
我們知道Java虛擬機(jī)棧是線程私有的,它的生命周期與線程相同。
虛擬機(jī)棧是Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)一部分,它描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程就對(duì)應(yīng)著一個(gè)幀棧在虛擬機(jī)中入棧到出棧的過(guò)程。
加載和存儲(chǔ)指令用于將數(shù)據(jù)在幀棧中的局部變量和操作數(shù)棧之前來(lái)回傳輸,這類指令包括如下內(nèi)容:
將一個(gè)局部變量加載到操作棧:iload, iload_
、aload、aload_ 等 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表:istore、 istore_
、astore、astore_ 等 將一個(gè)常量加載到操作數(shù)棧:ldc、iconst_等
來(lái)看看當(dāng)初我做錯(cuò)的那道題
public?class?Test?{
??public?static?void?main(String[]?args)?{
??????System.out.println(getNumber());
??}
??public?static?int?getNumber()?{
????int?x;
????try?{
??????x=1;
??????return?x;
????}?catch?(Exception?e)?{
??????x=2;
??????return?x;
????}
????finally{
??????x=3;
????}
??}
}
上面的代碼輸出結(jié)果是1,我們首先來(lái)看看它的字節(jié)碼是怎樣的?
?public?static?int?getNumber();
??Code:
???0:?iconst_1
???1:?istore_0
???2:?iload_0
???3:?istore_1
???4:?iconst_3
???5:?istore_0
???6:?iload_1
???7:?ireturn
???8:?astore_1
???9:?iconst_2
??10:?istore_0
??11:?iload_0
??12:?istore_2
??13:?iconst_3
??14:?istore_0
??15:?iload_2
??16:?ireturn
??17:?astore_3
??18:?iconst_3
??19:?istore_0
??20:?aload_3
??21:?athrow
Exception?table:
???from????to??target?type
???????0?????4?????8???Class?java/lang/Exception
???????0?????4????17???any
???????8????13????17???any
??}
我來(lái)根據(jù)字節(jié)碼來(lái)分析下代碼是如何運(yùn)行的:
操作數(shù)棧: 先進(jìn)后出的一個(gè)數(shù)據(jù)結(jié)構(gòu)
局部變量表: 可以認(rèn)為是一個(gè)數(shù)組,下標(biāo)從0開始
以下是執(zhí)行每一條指令的時(shí)候操作數(shù)棧和局部變量表的變化情況:
iconst_1(將int類型數(shù)字1放入操作數(shù)棧頂):
操作數(shù)棧:?1
局部變量表:
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:1
iload_0(將變量表第1個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?1
局部變量表:
istore_1(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第2個(gè)本地變量):
操作數(shù)棧:
局部變量表:?null?1
iconst_3(將int類型數(shù)字3放入操作數(shù)棧頂):
操作數(shù)棧:?3
局部變量表:?null?1
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:?3?1
iload1(將變量表第2個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?1
局部變量表:?3
ireturn(從棧頂返回int型數(shù)字,方法結(jié)束):
返回1
可以看到ireturn后面的代碼就不會(huì)被執(zhí)行了,我們也就不進(jìn)行翻譯了。實(shí)際上try-catch-finally字節(jié)碼塊中是沒(méi)有finally的, 根據(jù)字節(jié)碼我們可以將代碼簡(jiǎn)化成這樣,記住這段代碼,try-finally翻譯過(guò)來(lái)就是這樣的
public?static?int?getNumber()?{
??int?x;
??int?returnValue;
??try?{
????x=1;
????returnValue?=?x;
????x?=?3;
????return?returnValue;
??}?catch?(Exception?e)?{
????x=2;
????return?x;
??}
}
那如果將代碼變成這樣呢?
public?static?int?getNumber()?{
??int?x;
??try?{
????x=1;
????return?x;
??}?catch?(Exception?e)?{
????x=2;
????return?x;
??}
??finally{
????x=3;
????return?x;
??}
}
我相信你能知道輸出的結(jié)果是3,我們同樣來(lái)看下字節(jié)碼是怎樣的?
public?static?int?getNumber();
??Code:
?????0:?iconst_1
?????1:?istore_0
?????2:?iload_0
?????3:?istore_1
?????4:?iconst_3
?????5:?istore_0
?????6:?iload_0
?????7:?ireturn
?????8:?astore_1
?????9:?iconst_2
????10:?istore_0
????11:?iload_0
????12:?istore_2
????13:?iconst_3
????14:?istore_0
????15:?iload_0
????16:?ireturn
????17:?astore_3
????18:?iconst_3
????19:?istore_0
????20:?iload_0
????21:?ireturn
??Exception?table:
?????from????to??target?type
?????????0?????4?????8???Class?java/lang/Exception
?????????0?????4????17???any
?????????8????13????17???any
}
同樣的我們只分析執(zhí)行到的部分
iconst_1(將int類型數(shù)字1放入操作數(shù)棧頂):
操作數(shù)棧:?1
局部變量表:
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:1
iload_0(將變量表第1個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?1
局部變量表:
istore_1(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第2個(gè)本地變量):
操作數(shù)棧:?
局部變量表:null 1
iconst_3(將int類型數(shù)字3放入操作數(shù)棧頂):
操作數(shù)棧:?3
局部變量表:null 1
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:3 1
#?注意這里和上面字節(jié)碼的不同之處在于上面是加載變量表中的第二個(gè)int類型本地變量
iload_0(將變量表第1個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?3
局部變量表:1
ireturn(從棧頂返回int型數(shù)字,方法結(jié)束):
返回3
上面都只是分析了try-finally,我們接著分析下try-catch-finally是如何的
public?static?int?getNumber()?{
??int?x;
??try?{
????x?=?1/0;
????return?x;
??}?catch?(Exception?e)?{
????x?=?2;
????return?x;
??}?finally?{
????x?=?3;
????return?x;
??}
}
之前的代碼很明顯不會(huì)拋出異常,所以就用不到異常表中的內(nèi)容,但是這里肯定是產(chǎn)生異常(1/0),而在java中對(duì)異常的處理在字節(jié)碼層面是使用Exception Table來(lái)完成的。
public?static?int?getNumber();
Code:
?0:?iconst_1
?1:?iconst_0
?2:?idiv
?3:?istore_0
?4:?iload_0
?5:?istore_1
?6:?iconst_3
?7:?istore_0
?8:?iload_0
?9:?ireturn
10:?astore_1
11:?iconst_2
12:?istore_0
13:?iload_0
14:?istore_2
15:?iconst_3
16:?istore_0
17:?iload_0
18:?ireturn
19:?astore_3
20:?iconst_3
21:?istore_0
22:?iload_0
23:?ireturn
Exception?table:
?from????to??target?type
?????0?????6????10???Class?java/lang/Exception
?????0?????6????19???any
????10????15????19???any
}
這個(gè)異常表(Exception table)含義是如果當(dāng)字節(jié)碼在第from行到第to行之間(不包含to行)出現(xiàn)了類型為type或者其子類的異常則轉(zhuǎn)到第target行繼續(xù)處理。當(dāng)type的值為any時(shí),代表任意異常情況都需要轉(zhuǎn)向到target處進(jìn)行處理。
異常表也相當(dāng)于指明了代碼有可能的分支執(zhí)行情況。老規(guī)矩,我們一行一行來(lái)分析字節(jié)碼
iconst_1(將int類型數(shù)字1放入操作數(shù)棧頂):
操作數(shù)棧:?1
局部變量表:
iconst_0(將int類型數(shù)字0放入操作數(shù)棧頂):
操作數(shù)棧:?0?1
局部變量表:
idiv(將操作數(shù)棧頂兩int型數(shù)值相除,并將結(jié)果壓入棧頂):
操作數(shù)棧:
局部變量表:
經(jīng)過(guò)上面的操作(1/0)拋出異常,此時(shí)根據(jù)異常表,執(zhí)行第10行的字節(jié)碼
astore_1(將棧頂引用型數(shù)字存入變量表第2個(gè)本地變量,因?yàn)闂m敒榭眨远紴榭?:
操作數(shù)棧:
局部變量表:
iconst_2(將int類型數(shù)字2放入操作數(shù)棧頂)
操作數(shù)棧:?2
局部變量表:
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:2
iload_0(將變量表第1個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?2
局部變量表:
istore_2(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:2
iconst_3(將int類型數(shù)字3放入操作數(shù)棧頂):
操作數(shù)棧:?3
局部變量表:2
istore_0(將操作數(shù)棧頂int型數(shù)字出棧存入變量表第1個(gè)本地變量):
操作數(shù)棧:?
局部變量表:3
iload_0(將變量表第1個(gè)int型本地變量推送至棧頂):
操作數(shù)棧:?3
局部變量表:
ireturn(從棧頂返回int型數(shù)字,方法結(jié)束):
返回3
經(jīng)過(guò)上面的分析我們可以知道,try-catch-finally就是很普通的指令跳轉(zhuǎn)而已,我們最需要記住的是當(dāng)return的時(shí)候,實(shí)際上并不會(huì)馬上return,而是會(huì)將這個(gè)結(jié)果存入這個(gè)臨時(shí)變量,然后再返回這個(gè)臨時(shí)變量。
由于本文所舉例的代碼中使用的是基本類型,所以對(duì)值的修改看上去沒(méi)有起作用,但是如果“i”是對(duì)可變類對(duì)象的引用,并且對(duì)象的內(nèi)容在finally塊中進(jìn)行了更改,則這些更改也將反映在返回的值中。
