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

按我的個人理解,這個題目本身就問的有點問題,因爲這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,子類是繼承不到的

然後我們將報錯的代碼刪除,運行結果如下:

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成員變量。

刪除掉錯誤的代碼,運行結果如下:

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次賦值時報錯。

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");
        }
    }
}

運行結果如下:

也就是說,以上示例中,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()可能永遠不被執行,顯然指望它做收尾工作是靠不住的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章