1. 程式人生 > 其它 >JAVA面試題解惑——final、finally和finalize的區別

JAVA面試題解惑——final、finally和finalize的區別

final、finally和finalize的區別是什麼?

這是一道再經典不過的面試題了,我們在各個公司的面試題中幾乎都能看到它的身影。final、finally和finalize雖然長得像孿生三兄弟一樣,但是它們的含義和用法卻是大相徑庭。這一次我們就一起來回顧一下這方面的知識。

final關鍵字

我們首先來說說final。它可以用於以下四個地方:

定義變數,包括靜態的和非靜態的。

定義方法的引數。

定義方法。

定義類。

我們依次來回顧一下每種情況下final的作用。首先來看第一種情況,如果final修飾的是一個基本型別,就表示這個變數被賦予的值是不可變 的,即它是個常量;如果final修飾的是一個物件,就表示這個變數被賦予的引用是不可變的,這裡需要提醒大家注意的是,不可改變的只是這個變數所儲存的 引用,並不是這個引用所指向的物件。在第二種情況下,final的含義與第一種情況相同。實際上對於前兩種情況,有一種更貼切的表述final的含義的描 述,那就是,如果一個變數或方法引數被final修飾,就表示它只能被賦值一次,但是JAVA虛擬機器為變數設定的預設值不記作一次賦值。

被final修飾的變數必須被初始化。初始化的方式有以下幾種:

在定義的時候初始化。

final變數可以在初始化塊中初始化,不可以在靜態初始化塊中初始化。

靜態final變數可以在靜態初始化塊中初始化,不可以在初始化塊中初始化。

final變數還可以在類的構造器中初始化,但是靜態final變數不可以。

通過下面的程式碼可以驗證以上的觀點:

Java程式碼

public class FinalTest {  
    // 在定義時初始化  
    public final int A = 10;  
    public final int B;  
    // 在初始化塊中初始化  
    {  
        B = 20;  
    }  
    // 非靜態final變數不能在靜態初始化塊中初始化  
    // public final int C;  
    // static {  
    // C = 30;  
    // }  
    // 靜態常量,在定義時初始化  
    public static final int STATIC_D = 40;  
    public static final int STATIC_E;  
    // 靜態常量,在靜態初始化塊中初始化  
    static {  
        STATIC_E = 50;  
    }  
    // 靜態變數不能在初始化塊中初始化  
    // public static final int STATIC_F;  
    // {  
    // STATIC_F = 60;  
    // }  
    public final int G;  
    // 靜態final變數不可以在構造器中初始化  
    // public static final int STATIC_H;  
    // 在構造器中初始化  
    public FinalTest() {  
        G = 70;  
        // 靜態final變數不可以在構造器中初始化  
        // STATIC_H = 80;  
        // 給final的變數第二次賦值時,編譯會報錯  
        // A = 99;  
        // STATIC_D = 99;  
    }  
    // final變數未被初始化,編譯時就會報錯  
    // public final int I;  
    // 靜態final變數未被初始化,編譯時就會報錯  
    // public static final int STATIC_J;  
}  

我們執行上面的程式碼之後出了可以發現final變數(常量)和靜態final變數(靜態常量)未被初始化時,編譯會報錯。

用final修飾的變數(常量)比非final的變數(普通變數)擁有更高的效率,因此我們在實際程式設計中應該儘可能多的用常量來代替普通變數,這也是一個很好的程式設計習慣。

當final用來定義一個方法時,會有什麼效果呢?正如大家所知,它表示這個方法不可以被子類重寫,但是它這不影響它被子類繼承。我們寫段程式碼來驗證一下:

Java程式碼

class ParentClass {  
    public final void TestFinal() {  
        System.out.println("父類--這是一個final方法");  
    }  
}  
public class SubClass extends ParentClass {  
    /** 
     * 子類無法重寫(override)父類的final方法,否則編譯時會報錯 
     */  
    // public void TestFinal() {  
    // System.out.println("子類--重寫final方法");  
    // }  
    public static void main(String[] args) {  
        SubClass sc = new SubClass();  
        sc.TestFinal();  
    }  
}  

這裡需要特殊說明的是,具有private訪問許可權的方法也可以增加final修飾,但是由於子類無法繼承private方法,因此也無法重寫 它。編譯器在處理private方法時,是按照final方法來對待的,這樣可以提高該方法被呼叫時的效率。不過子類仍然可以定義同父類中的 private方法具有同樣結構的方法,但是這並不會產生重寫的效果,而且它們之間也不存在必然聯絡。

最後我們再來回顧一下final用於類的情況。這個大家應該也很熟悉了,因為我們最常用的String類就是final的。由於final類不允 許被繼承,編譯器在處理時把它的所有方法都當作final的,因此final類比普通類擁有更高的效率。final的類的所有方法都不能被重寫,但這並不 表示final的類的屬性(變數)值也是不可改變的,要想做到final類的屬性值不可改變,必須給它增加final修飾,請看下面的例子:

Java程式碼

public final class FinalTest {  
    int i = 10;  
    public static void main(String[] args) {  
        FinalTest ft = new FinalTest();  
        ft.i = 99;  
        System.out.println(ft.i);  
    }  
}  

執行上面的程式碼試試看,結果是99,而不是初始化時的10。

finally語句

接下來我們一起回顧一下finally的用法。這個就比較簡單了,它只能用在try/catch語句中,並且附帶著一個語句塊,表示這段語句最終總是被執行。請看下面的程式碼:

Java程式碼

public final class FinallyTest {  
    public static void main(String[] args) {  
        try {  
            throw new NullPointerException();  
        } catch (NullPointerException e) {  
            System.out.println("程式丟擲了異常");  
        } finally {  
            System.out.println("執行了finally語句塊");  
        }  
    }  
}  

執行結果說明了finally的作用:

程式丟擲了異常

執行了finally語句塊

請大家注意,捕獲程式丟擲的異常之後,既不加處理,也不繼續向上丟擲異常,並不是良好的程式設計習慣,它掩蓋了程式執行中發生的錯誤,這裡只是方便演示,請不要學習。

那麼,有沒有一種情況使finally語句塊得不到執行呢?大家可能想到了return、continue、break這三個可以打亂程式碼順序執行語句的規律。那我們就來試試看,這三個語句是否能影響finally語句塊的執行:

Java程式碼

public final class FinallyTest {  
    // 測試return語句  
    public ReturnClass testReturn() {  
        try {  
            return new ReturnClass();  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            System.out.println("執行了finally語句");  
        }  
        return null;  
    }  
    // 測試continue語句  
    public void testContinue() {  
        for (int i = 0; i < 3; i++) {  
            try {  
                System.out.println(i);  
                if (i == 1) {  
                    continue;  
                }  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                System.out.println("執行了finally語句");  
            }  
        }  
    }  
    // 測試break語句  
    public void testBreak() {  
        for (int i = 0; i < 3; i++) {  
            try {  
                System.out.println(i);  
                if (i == 1) {  
                    break;  
                }  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                System.out.println("執行了finally語句");  
            }  
        }  
    }  
    public static void main(String[] args) {  
        FinallyTest ft = new FinallyTest();  
        // 測試return語句  
        ft.testReturn();  
        System.out.println();  
        // 測試continue語句  
        ft.testContinue();  
        System.out.println();  
        // 測試break語句  
        ft.testBreak();  
    }  
}  
class ReturnClass {  
    public ReturnClass() {  
        System.out.println("執行了return語句");  
    }  
}  

上面這段程式碼的執行結果如下:

執行了return語句

執行了finally語句

0

執行了finally語句

1

執行了finally語句

2

執行了finally語句

0

執行了finally語句

1

執行了finally語句

很明顯,return、continue和break都沒能阻止finally語句塊的執行。從輸出的結果來看,return語句似乎在 finally語句塊之前執行了,事實真的如此嗎?我們來想想看,return語句的作用是什麼呢?是退出當前的方法,並將值或物件返回。如果 finally語句塊是在return語句之後執行的,那麼return語句被執行後就已經退出當前方法了,finally語句塊又如何能被執行呢?因 此,正確的執行順序應該是這樣的:編譯器在編譯return new ReturnClass();時,將它分成了兩個步驟,new ReturnClass()和return,前一個建立物件的語句是在finally語句塊之前被執行的,而後一個return語句是在finally語 句塊之後執行的,也就是說finally語句塊是在程式退出方法之前被執行的。同樣,finally語句塊是在迴圈被跳過(continue)和中斷 (break)之前被執行的。

finalize方法

最後,我們再來看看finalize,它是一個方法,屬於java.lang.Object類,它的定義如下:

Java程式碼

protected void finalize() throws Throwable { }  

眾所周知,finalize()方法是GC(garbage collector)執行機制的一部分,關於GC的知識我們將在後續的章節中來回顧。

在此我們只說說finalize()方法的作用是什麼呢?

finalize()方法是在GC清理它所從屬的物件時被呼叫的,如果執行它的過程中丟擲了無法捕獲的異常(uncaught exception),GC將終止對改物件的清理,並且該異常會被忽略;直到下一次GC開始清理這個物件時,它的finalize()會被再次呼叫。

請看下面的示例:

Java程式碼

public final class FinallyTest {  
    // 重寫finalize()方法  
    protected void finalize() throws Throwable {  
        System.out.println("執行了finalize()方法");  
    }  
    public static void main(String[] args) {  
        FinallyTest ft = new FinallyTest();  
        ft = null;  
        System.gc();  
    }  
}  

執行結果如下:

執行了finalize()方法

程式呼叫了java.lang.System類的gc()方法,引起GC的執行,GC在清理ft物件時呼叫了它的finalize()方法,因此才有了上面的輸出結果。呼叫System.gc()等同於呼叫下面這行程式碼:

Java程式碼

Runtime.getRuntime().gc();  

呼叫它們的作用只是建議垃圾收集器(GC)啟動,清理無用的物件釋放記憶體空間,但是GC的啟動並不是一定的,這由JAVA虛擬機器來決定。直到 JAVA虛擬機器停止執行,有些物件的finalize()可能都沒有被執行過,那麼怎樣保證所有物件的這個方法在JAVA虛擬機器停止執行之前一定被呼叫 呢?答案是我們可以呼叫System類的另一個方法:

Java程式碼

public static void runFinalizersOnExit(boolean value) {  
    //other code  
}  

給這個方法傳入true就可以保證物件的finalize()方法在JAVA虛擬機器停止執行前一定被運行了,不過遺憾的是這個方法是不安全的,它會導致有用的物件finalize()被誤呼叫,因此已經不被贊成使用了。

由於finalize()屬於Object類,因此所有類都有這個方法,Object的任意子類都可以重寫(override)該方法,在其中釋放系統資源或者做其它的清理工作,如關閉輸入輸出流。

通過以上知識的回顧,我想大家對於final、finally、finalize的用法區別已經很清楚了。