【Java面試題系列】:Java中final finally finalize的區別

周偉偉的部落格發表於2019-05-15

本篇為【Java面試題系列】第三篇,文中如有錯誤,歡迎指正。

第一篇連結:【Java面試題系列】:Java基礎知識常見面試題彙總 第一篇

第二篇連結:【Java面試題系列】:Java基礎知識常見面試題彙總 第二篇

按我的個人理解,這個題目本身就問的有點問題,因為這3個關鍵字之間沒啥關係,是相對獨立的,我猜想這道題的初衷應該是想了解面試者對Java中final finally finalize的使用方法的掌握情況,只是因為3個關鍵字比較像,而成了現在網上流傳的題目“Java中final finally finalize的區別”。

既然是想了解面試者對Java中final finally finalize的使用方法的掌握情況,那麼本篇我們就分別講解下final,finally,finalize的使用方法。

1.final用法

我們先看下final的英文釋義:最終的;決定性的;不可更改的,不禁要推測被final修飾的變數,方法或者類是不是不可修改的呢?

1.1final修飾類

在Java中,被final修飾的類,不能被繼承,也就是final類的成員方法沒有機會被繼承,也沒有機會被重寫。

在設計類的時候,如果這個類不需要有子類,類的實現細節不允許改變,那麼就可以設計為final類。

如我們在開發中經常使用的String類就是final類,以下為部分原始碼:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    ......
}    

1.2final修飾方法

在Java中,被final修飾的方法,可以被繼承,但不能被子類重寫(覆蓋)。

在設計方法時,如果這個方法不希望被子類重寫(覆蓋),那麼就可以設計為final方法。

舉個具體的例子,我們新建個父類Animal如下:

package com.zwwhnly.springbootdemo;

public class Animal {
    public void eat() {
        System.out.println("Animal eat.");
    }

    public void call() {
        System.out.println("Animal call.");
    }

    public final void fly() {
        System.out.println("Animal fly.");
    }

    private final void swim() {
        System.out.println("Animal swim.");
    }
}

然後定義一個子類Cat繼承Animal類,程式碼如下:

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    @Override
    public void fly() {
        System.out.println("Cat fly.");
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat();
        cat.call();
        cat.fly();
        cat.swim();
    }
}

我們會發現,以上程式碼中有2個錯誤

1)當我們重寫fly()方法時,因為父類的fly()方法被定義為final方法,重寫時會編譯錯誤

2)cat.swim();報錯,因為父類的swim()方法被定義為private,子類是繼承不到的

【Java面試題系列】:Java中final finally finalize的區別

然後我們將報錯的程式碼刪除,執行結果如下:

Cat eat.

Animal call.

Animal fly.

也就是eat()方法被子類重寫了,繼承了父類的成員方法call()和final方法fly()。

但是值得注意的是,在子類Cat中,我們是可以重新定義父類的私有final方法swim()的,不過此時明顯不是重寫(你可以加@Override試試,會編譯報錯),而是子類自己的成員方法swim()。

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    public void swim() {
        System.out.println("Cat swim.");
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat();
        cat.call();
        cat.fly();
        cat.swim();
    }
}

此時的執行結果為:

Cat eat.

Animal call.

Animal fly.

Cat swim.

1.3final修飾成員變數

用final修飾的成員變數沒有預設值,可以在宣告時賦值或者在建構函式中賦值,但必須賦值且只能被賦值1次,賦值後無法修改。

我們修改下1.2中的Cat類程式碼,定義2個final成員變數,1個宣告完立即賦值,1個在建構函式中賦值:

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    private final int age = 1;
    private final String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    public static void main(String[] args) {
        Cat whiteCat = new Cat("小白");
        whiteCat.age = 2;
        System.out.println(whiteCat.age);
        System.out.println(whiteCat.name);

        Cat blackCat = new Cat("小黑");
        blackCat.name = "小黑貓";
        System.out.println(blackCat.age);
        System.out.println(blackCat.name);
    }
}

以上程式碼有2個編譯錯誤,1個是whiteCat.age = 2;修改成員變數age時,另1個是blackCat.name = "小黑貓";修改成員變數name時,都提示不能修改final成員變數。

【Java面試題系列】:Java中final finally finalize的區別

刪除掉錯誤的程式碼,執行結果如下:

1

小白

1

小黑

1.4final修飾區域性變數

被final修飾的區域性變數,既可以在宣告時立即賦值,也可以先宣告,後賦值,但只能賦值一次,不可以重複賦值。

修改下Cat類的eat()方法如下:

@Override
public void eat() {

    final String breakfast;
    final String lunch = "午餐";
    breakfast = "早餐";
    lunch = "午餐2";
    breakfast = "早餐2";

    System.out.println("Cat eat.");
}

以上程式碼中2個錯誤,1個是lunch = "午餐2";,1個是breakfast = "早餐2";,都是對final區域性變數第2次賦值時報錯。

【Java面試題系列】:Java中final finally finalize的區別

1.5final修飾方法引數

方法引數其實也是區域性變數,因此final修飾方法引數和1.4中final修飾區域性變數的使用類似,即方法中只能使用方法的引數值,但不能修改引數值。

在Cat類中新增方法printCatName,將方法引數修飾為final:

public static void main(String[] args) {
    Cat whiteCat = new Cat("小白");
    whiteCat.printCatName(whiteCat.name);
}

public void printCatName(final String catName) {
    //catName = "修改catName";    // 該行語句會報錯
    System.out.println(catName);
}

執行結果:

小白

2.finally用法

提起finally,大家都知道,這是Java中處理異常的,通常和try,catch一起使用,主要作用是不管程式碼發不發生異常,都會保證finally中的語句塊被執行。

你是這樣認為的嗎?說實話,哈哈。

那麼問題來了,finally語句塊一定會被執行嗎?,答案是不一定

讓我們通過具體的示例來證明該結論。

2.1在 try 語句塊之前返回(return)或者丟擲異常,finally不會被執行

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        /*if (i == 1) {
            return 0;
        }*/
        System.out.println("the previous statement of try block");
        i = i / 0;
        try {
            System.out.println("try block");
            return i;
        } finally {
            System.out.println("finally block");
        }
    }
}

執行結果如下:

【Java面試題系列】:Java中final finally finalize的區別

也就是說,以上示例中,finally語句塊沒有被執行。

然後我們將上例中註釋的程式碼取消註釋,此時執行結果為:

return value of test():0

finally語句塊還是沒有被執行,因此,我們可以得出結論:

只有與 finally 相對應的 try 語句塊得到執行的情況下,finally 語句塊才會執行。

以上兩種情況,都是在 try 語句塊之前返回(return)或者丟擲異常,所以 try 對應的 finally 語句塊沒有執行。

2.2與 finally 相對應的 try 語句塊得到執行,finally不一定會被執行

那麼,與 finally 相對應的 try 語句塊得到執行的情況下,finally 語句塊一定會執行嗎?答案仍然是不一定。

看下下面這個例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        try {
            System.out.println("try block");
            System.exit(0);
            return i;
        } finally {
            System.out.println("finally block");
        }
    }
}

執行結果為:

try block

finally語句塊還是沒有被執行,為什麼呢?因為我們在try語句塊中執行了System.exit(0);,終止了Java虛擬機器的執行。當然,一般情況下,我們的應用程式中是不會呼叫System.exit(0);的,那麼,如果不呼叫這個方法,finally語句塊一定會被執行嗎?

答案當然還是不一定,當一個執行緒在執行 try 語句塊或者 catch 語句塊時被打斷(interrupted)或者被終止(killed),與其相對應的 finally 語句塊可能不會執行。還有更極端的情況,就是線上程執行 try 語句塊或者 catch 語句塊時,突然當機或者斷電,finally 語句塊肯定不會執行了。當然,當機或者斷電屬於極端情況,在這裡只是為了證明,finally語句塊不一定會被執行。

2.3try語句塊或者catch語句塊中有return語句

如果try語句塊中有return語句, 是return語句先執行還是finally語句塊先執行呢?

帶著這個問題,我們看下如下這個例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        try {
            System.out.println("try block");
            return;
        } finally {
            System.out.println("finally block");
        }
    }
}

執行結果:

try block

finally block

結論:finally 語句塊在 try 語句塊中的 return 語句之前執行。

如果catch語句塊中有return語句,是return語句先執行還是finally語句塊先執行呢?

帶著這個問題,我們看下如下這個例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        try {
            System.out.println("try block");
            i = i / 0;
            return 1;
        } catch (Exception e) {
            System.out.println("catch block");
            return 2;
        } finally {
            System.out.println("finally block");
        }
    }
}

執行結果:

try block

catch block

finally block

return value of test():2

結論:finally 語句塊在 catch 語句塊中的 return 語句之前執行。

通過上面2個例子,我們可以看出,其實 finally 語句塊是在 try 或者 catch 中的 return 語句之前執行的。更加一般的說法是,finally 語句塊應該是在控制轉移語句之前執行,控制轉移語句除了 return 外,還有 break ,continue和throw。

2.4其它幾個例子

示例1:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }
}

執行結果:

return value of getValue():1

示例2:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
        }
    }
}

執行結果:

return value of getValue():1

也許你會好奇,應該會返回2,怎麼返回1了呢?可以借鑑下以下內容來理解(牽扯到了Java虛擬機器如何編譯finally語句塊):

實際上,Java 虛擬機器會把 finally 語句塊作為 subroutine(對於這個 subroutine 不知該如何翻譯為好,乾脆就不翻譯了,免得產生歧義和誤解。)直接插入到 try 語句塊或者 catch 語句塊的控制轉移語句之前。但是,還有另外一個不可忽視的因素,那就是在執行 subroutine(也就是 finally 語句塊)之前,try 或者 catch 語句塊會保留其返回值到本地變數表(Local Variable Table)中。待 subroutine 執行完畢之後,再恢復保留的返回值到運算元棧中,然後通過 return 或者 throw 語句將其返回給該方法的呼叫者(invoker)。請注意,前文中我們曾經提到過 return、throw 和 break、continue 的區別,對於這條規則(保留返回值),只適用於 return 和 throw 語句,不適用於 break 和 continue 語句,因為它們根本就沒有返回值。

示例3:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            i = 4;
        } finally {
            i++;
            return i;
        }
    }
}

執行結果:

return value of getValue():5

示例4:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            i = 4;
        } finally {
            i++;
        }
        return i;
    }
}

執行結果:

return value of getValue():5

示例5:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println(test());
    }

    public static String test() {
        try {
            System.out.println("try block");
            return test1();
        } finally {
            System.out.println("finally block");
        }
    }

    public static String test1() {
        System.out.println("return statement");
        return "after return";
    }
}

try block

return statement

finally block

after return

2.5總結

  1. finally語句塊不一定會被執行
  2. finally 語句塊在 try 語句塊中的 return 語句之前執行。
  3. finally 語句塊在 catch 語句塊中的 return 語句之前執行。
  4. 注意控制轉移語句 return ,break ,continue,throw對執行順序的影響

3.finalize用法

finalize()是Object類的一個方法,因此所有的類都繼承了這個方法。

protected void finalize() throws Throwable { }

finalize()主要用於在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在確定這個物件沒有被引用時對這個物件呼叫的。

子類覆蓋 finalize() 方法以整理系統資源或者執行其他清理工作。finalize() 方法是在垃圾收集器刪除物件之前對這個物件呼叫的。

當垃圾回收器(GC)決定回收某物件時,就會執行該物件的finalize()方法。

不過在Java中,如果記憶體總是充足的,那麼垃圾回收可能永遠不會進行,也就是說filalize()可能永遠不被執行,顯然指望它做收尾工作是靠不住的。

4.參考連結

java中的final如何使用和理解

Java中的final關鍵字

解析Java finally

java finalize方法的使用

相關文章