面試題:final、finally、finalize的區別
面試考察點
考察目的: 了解求職者對Java基礎的了解。
考察范圍: 工作1-3年的Java程序員。
背景知識
final
/finally
在工作中幾乎無時無刻不再使用,因此即便是沒有系統化的梳理這個問題,也能回答出一些內容。
但是finalize
就接觸得非常少,接下來我們對這幾個關鍵字逐一進行分析。
final關鍵字
final關鍵字代表着不可變性。
在面試題系列:工作5年,第一次這么清醒的理解final關鍵字?.這篇文章中, 我詳細的進行了分析,建議大家去看這篇文章,這里就不重復分析了。
finally關鍵字
finally
關鍵字用在try
語句塊后面,它的常用形式是
try{
}catch(){
}finally{
}
以及下面這種形式。
try{
}finally{
}
finally語句塊中的代碼,無論try或者catch代碼塊中是否有異常,finally語句塊中的代碼一定會被執行,所以它一般用於清理工作、關閉鏈接等類型的語句。
它的特點:
finally
語句一定會伴隨try
語句出現。try
語句不能單獨使用,必須配合catch
語句或finally
語句。try
語句可以單獨與catch
語句一起使用,也可以單獨與finally
語句一起使用,也可以三者一起使用。
finally 實戰思考
為了加深大家對於finally
關鍵字的理解,我們來看下面這段代碼。
思考一下,下面這段代碼,打印的結果分別是多少?
public class FinallyExample {
public static void main(String arg[]){
System.out.println(getNumber(0));
System.out.println(getNumber(1));
System.out.println(getNumber(2));
System.out.println(getNumber(4));
}
public static int getNumber(int num){
try{
int result=2/num;
return result;
}catch(Exception exception){
return 0;
}finally{
if(num==0){
return -1;
}
if(num==1){
return 1;
}
}
}
}
正確答案分別是:
-1
: 傳入num=0
,此時會報錯java.lang.ArithmeticException: / by zero
。因此進入到catch
捕獲該異常。由於finally
語句塊一定會被執行,因此進入到finally
語句塊,返回-1
。1
:傳入num=1
,此時程序運行正常,由於finally
語句塊一定會被執行,因此進入到finally
代碼塊,得到結果1
。1
:傳入num=2
,此時程序運行正常,result=1
,由於finally
語句塊一定會被執行,因此進入到finally
代碼塊,但是finally
語句塊並沒有觸發對結果的修改,所以返回結果為1
。0
:傳入num=4
,此時程序運行正常,result=0
(因為2/4=0.5,轉換為int后得到0),由於finally
語句塊一定會被執行,因此進入到finally
代碼塊,但是finally
語句塊並沒有觸發對結果的修改,所以返回結果為0
。
什么情況下finally
不會執行
finally
代碼塊,是否有存在不會被執行的情況呢?
System.exit()
來看下面這段代碼:
public class FinallyExample {
public static void main(String arg[]){
System.out.println(getNumber(0));
}
public static int getNumber(int num){
try{
int result=2/num;
return result;
}catch(Exception exception){
System.out.println("觸發異常執行");
System.exit(0);
return 0;
}finally{
System.out.println("執行finally語句塊");
}
}
}
在catch
語句塊中,增加了System.exit(0)
代碼,執行結果如下
觸發異常執行
可以發現,在這種情況下,並沒有執行finally
語句塊。
擴展知識,為什么
System.exit(0)
會破壞finally
呢?來看一下源碼以及注釋。
/** * Terminates the currently running Java Virtual Machine. The * argument serves as a status code; by convention, a nonzero status * code indicates abnormal termination. * <p> * This method calls the <code>exit</code> method in class * <code>Runtime</code>. This method never returns normally. * <p> * The call <code>System.exit(n)</code> is effectively equivalent to * the call: * <blockquote><pre> * Runtime.getRuntime().exit(n) * </pre></blockquote> * * @param status exit status. * @throws SecurityException * if a security manager exists and its <code>checkExit</code> * method doesn't allow exit with the specified status. * @see java.lang.Runtime#exit(int) */ public static void exit(int status) { Runtime.getRuntime().exit(status); }
該方法用來結束當前正在運行的
Java JVM
。如果 status 是非零參數,那么表示是非正常退出。
System.exit(0) : 將整個虛擬機里的內容都關掉,內存都釋放掉!正常退出程序。
System.exit(1) : 非正常退出程序
System.exit(-1) :非正常退出程序
由於當前JVM已經結束了,因此程序代碼自然不能繼續執行。
守護線程被中斷
先來看下面這段代碼:
public class FinallyExample {
public static void main(String[] args) {
Thread t = new Thread(new Task());
t.setDaemon(true); //置為守護線程
t.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
}
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println("執行 run()方法");
try {
System.out.println("執行 try 語句塊");
TimeUnit.SECONDS.sleep(5); //阻塞5s
} catch(InterruptedException e) {
System.out.println("執行 catch 語句塊");
throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
} finally {
System.out.println("執行 finally 語句塊");
}
}
}
運行結果如下:
執行 run()方法
執行 try 語句塊
從結果發現,finally
語句塊中的代碼並沒有被執行?為什么呢?
守護線程的特性是:只要JVM中沒有任何非守護線程在運行,那么虛擬機會kill掉所有守護線程從而終止程序。換句話說,就是守護線程是否正在運行,都不影響JVM的終止。
在虛擬機中,垃圾回收線程就屬於守護線程。
在上述運行的程序中,執行邏輯描述如下:
- 線程
t
是守護線程,它開啟一個任務Task
執行,該線程t
在main
方法中執行,並且在睡眠1s之后,main
方法執行結束 Task
是一個守護線程的執行任務,該任務睡眠5s。
基於守護線程的特性,main
和task
都是守護線程,因此當main
線程執行結束后,並不會因為Task
這個線程還未執行結束而阻塞。而是在等待1s后,結束該進程。
這就使得Task
這個線程的代碼還未執行完成,但是JVM進程已結束,所以finally
語句塊沒有被執行。
finally執行順序
基於上述內容的理解,是不是自認為對finally
關鍵字掌握很好了?那我們在來看看下面這個問題。
這段代碼的執行結果是多少呢?
public class FinallyExample2 {
public int add() {
int x = 1;
try {
return ++x;
} catch (Exception e) {
System.out.println("執行catch語句塊");
++x;
} finally {
System.out.println("執行finally語句塊");
++x;
}
return x;
}
public static void main(String[] args) {
FinallyExample2 t = new FinallyExample2();
int y = t.add();
System.out.println(y);
}
}
上述程序運行的結果是:2
這個結果應該有點意外,因為按照finally
的語義,首先執行try
代碼塊,++x
后得到的結果應該是2, 接着再執行finally
語句塊,應該是在2的基礎上再+1,得到結果是3,那為什么是2?
在解答這個問題之前,先來看一下這段代碼的字節碼,使用javap -v FinallyExample2
.
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1 //iconst 指令將常量壓入棧中。
1: istore_1 //
2: iinc 1, 1 //執行++x操作
5: iload_1
6: istore_2
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #3 // String 執行finally語句塊
12: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: iinc 1, 1
18: iload_2
19: ireturn
20: astore_2
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: ldc #6 // String 執行catch語句塊
26: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: iinc 1, 1
32: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
35: ldc #3 // String 執行finally語句塊
37: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: iinc 1, 1
43: goto 60
46: astore_3
47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
50: ldc #3 // String 執行finally語句塊
52: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
55: iinc 1, 1
58: aload_3
59: athrow
60: iload_1
61: ireturn
Exception table:
from to target type
2 7 20 Class java/lang/Exception
2 7 46 any
20 32 46 any
簡單說明一下和本次案例分析有關的字節指令
iconst
,把常量壓入到棧中。istore
,棧頂的int數值存入局部變量表。iload
,把int類型的變量壓入到棧頂。iinc
,對局部變量表中index為i的元素加上n。ireturn
,返回一個int類型的值。astore
,將一個數值從操作數棧存儲到局部變量表。athrow
,拋出一個異常。aload
,將一個局部變量加載到操作棧。
了解了這些指令之后,再來分析上述字節碼的內容。
先來看第一步分,這部分是try
代碼塊中的指令。
0: iconst_1 //iconst 指令將常量壓入棧中。
1: istore_1 //
2: iinc 1, 1 //執行++x操作
5: iload_1
6: istore_2
上述指令的執行流程,圖解如下。
接下來繼續往下看字節碼,這個是在finally里面執行的指令。
15: iinc 1, 1
18: iload_2
19: ireturn
20: astore_2
從上述指令的圖解過程中可以看到,在finally
語句塊中雖然對x
的值做了累加,但是最終返回的時候,仍然是2.
后續剩余的指令,是異常表對應的執行指令,異常表的解讀方式是:
-
從2行到第7行,如果觸發了Exception,則會跳轉到20行的指令開始執行。
-
從2行到第7行,如果觸發了任何異常,則會跳轉到46行開始執行。
-
從20行到第32行,如果觸發了任何異常,則會跳轉到46行開始執行。
Exception table:
from to target type
2 7 20 Class java/lang/Exception
2 7 46 any
20 32 46 any
結論:從上述字節指令的執行過程中可以發現,當try中帶有return時,會先執行return前的代碼,然后暫時保存需要return的信息,再執行finally中的代碼,最后再通過return返回之前保存的信息。所以這里運行的結果是2,而不是3。
除此之外,還有其他的變體,比如:
public class FinallyExample2 {
public int add() {
int x = 1;
try {
return ++x;
} catch (Exception e) {
System.out.println("執行catch語句塊");
++x;
} finally {
System.out.println("執行finally語句塊");
++x;
return x;
}
}
public static void main(String[] args) {
FinallyExample2 t = new FinallyExample2();
int y = t.add();
System.out.println(y);
}
}
那,這段代碼運行結果是多少呢?
打印結果如下:
執行finally語句塊
3
結論:當finally中有return的時候,try中的return會失效,在執行完finally的return之后,就不會再執行try中的return。不過不推薦在finally中寫return,這會破壞程序的完整性,而且一旦finally里出現異常,會導致catch中的異常被覆蓋。
關於這個部分說解釋的內容,在JVM的中有Exceptions and finally
解釋。
If the try clause executes a return, the compiled code does the following:
- Saves the return value (if any) in a local variable.
- Executes a jsr to the code for the finally clause.
- Upon return from the finally clause, returns the value saved in the local variable.
簡單翻譯如下:
如果 try 語句里有 return,那么代碼的行為如下:
- 如果有返回值,就把返回值保存到局部變量中
- 執行 jsr 指令跳到 finally 語句里執行
- 執行完 finally 語句后,返回之前保存在局部變量表里的值
finalize方法
finalize 方法定義在 Object 類中,其方法定義如下:
protected void finalize() throws Throwable {
}
當一個類在被回收期間,這個方法就可能會被調用到。
它有使用規則是:
- 當對象不再被任何對象引用時,GC會調用該對象的finalize()方法
- finalize()是Object的方法,子類可以覆蓋這個方法來做一些系統資源的釋放或者數據的清理
- 可以在finalize()讓這個對象再次被引用,避免被GC回收;但是最常用的目的還是做cleanup
- Java不保證這個finalize()一定被執行;但是保證調用finalize的線程沒有持有任何user-visible同步鎖。
- 在finalize里面拋出的異常會被忽略,同時方法終止。
- 當finalize被調用之后,JVM會再一次檢測這個對象是否能被存活的線程訪問得到,如果不是,則清除該對象。也就是finalize只能被調用一次;也就是說,覆蓋了finalize方法的對象需要經過兩個GC周期才能被清除。
問題回答
面試題:final、finally、finalize的區別
回答:
-
final用來修飾類、方法、屬性,被final修飾的類,表示該類無法被繼承,被final修飾的屬性,表示該屬性無法被修改,被final修飾的方法,表示該方法無法被重寫
-
finally,它和try語句塊組成一個完整的語法,表示一定會被執行的代碼塊,當然也有方式可以破壞它的執行特性
- 通過System.exit
- 守護線程的終止
-
finalize方法,是一個類被回收期間可能會被調用的方法。
問題總結
一道面試題,要深挖下來,可以產生很多變體。
這篇文章不一定非常全面的涵蓋了所有可能的情況,但是各位讀者一定要注意,只有體系化的知識,才能創造價值
關注[跟着Mic學架構]公眾號,獲取更多精品原創