天天看點

工作5年的程式員感慨:final、finally、finalize面試這麼卷?

工作5年的程式員感慨:final、finally、finalize面試這麼卷?
面試題: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語句塊中的代碼一定會被執行,是以它一般用于清理工作、關閉連結等類型的語句。

它的特點:

  1. finally

    語句一定會伴随

    try

    語句出現。
  2. try

    語句不能單獨使用,必須配合

    catch

    語句或

    finally

    語句。
  3. 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. -1

    : 傳入

    num=0

    ,此時會報錯

    java.lang.ArithmeticException: / by zero

    。是以進入到

    catch

    捕獲該異常。由于

    finally

    語句塊一定會被執行,是以進入到

    finally

    語句塊,傳回

    -1

  2. 1

    :傳入

    num=1

    ,此時程式運作正常,由于

    finally

    finally

    代碼塊,得到結果

    1

  3. 1

    num=2

    ,此時程式運作正常,

    result=1

    ,由于

    finally

    finally

    代碼塊,但是

    finally

    語句塊并沒有觸發對結果的修改,是以傳回結果為

    1

  4. num=4

    result=0

    (因為2/4=0.5,轉換為int後得到0),由于

    finally

    finally

    finally

什麼情況下

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 是非零參數,那麼表示是非正常退出。
  1. System.exit(0) : 将整個虛拟機裡的内容都關掉,記憶體都釋放掉!正常退出程式。
  2. System.exit(1) : 非正常退出程式
  3. 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的終止。

在虛拟機中,垃圾回收線程就屬于守護線程。

在上述運作的程式中,執行邏輯描述如下:

  1. 線程

    t

    是守護線程,它開啟一個任務

    Task

    執行,該線程

    t

    main

    方法中執行,并且在睡眠1s之後,

    main

    方法執行結束
  2. 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
           

上述指令的執行流程,圖解如下。

工作5年的程式員感慨:final、finally、finalize面試這麼卷?

接下來繼續往下看位元組碼,這個是在finally裡面執行的指令。

15: iinc          1, 1
18: iload_2
19: ireturn
20: astore_2
           
工作5年的程式員感慨:final、finally、finalize面試這麼卷?

從上述指令的圖解過程中可以看到,在

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:
  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 語句後,傳回之前儲存在局部變量表裡的值

finalize方法

finalize 方法定義在 Object 類中,其方法定義如下:

protected void finalize() throws Throwable {
}
           

當一個類在被回收期間,這個方法就可能會被調用到。

它有使用規則是:

  1. 當對象不再被任何對象引用時,GC會調用該對象的finalize()方法
  2. finalize()是Object的方法,子類可以覆寫這個方法來做一些系統資源的釋放或者資料的清理
  3. 可以在finalize()讓這個對象再次被引用,避免被GC回收;但是最常用的目的還是做cleanup
  4. Java不保證這個finalize()一定被執行;但是保證調用finalize的線程沒有持有任何user-visible同步鎖。
  5. 在finalize裡面抛出的異常會被忽略,同時方法終止。
  6. 當finalize被調用之後,JVM會再一次檢測這個對象是否能被存活的線程通路得到,如果不是,則清除該對象。也就是finalize隻能被調用一次;也就是說,覆寫了finalize方法的對象需要經過兩個GC周期才能被清除。

問題回答

回答:

  1. final用來修飾類、方法、屬性,被final修飾的類,表示該類無法被繼承,被final修飾的屬性,表示該屬性無法被修改,被final修飾的方法,表示該方法無法被重寫
  2. finally,它和try語句塊組成一個完整的文法,表示一定會被執行的代碼塊,當然也有方式可以破壞它的執行特性
    1. 通過System.exit
    2. 守護線程的終止
  3. finalize方法,是一個類被回收期間可能會被調用的方法。

問題總結

一道面試題,要深挖下來,可以産生很多變體。

這篇文章不一定非常全面的涵蓋了所有可能的情況,但是各位讀者一定要注意,隻有體系化的知識,才能創造價值

關注[跟着Mic學架構]公衆号,擷取更多精品原創

工作5年的程式員感慨:final、finally、finalize面試這麼卷?

繼續閱讀