本篇博客講解1個常見的面試題: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.1 final修飾類
在Java中,被final修飾的類,不能被繼承,也就是final類的成員方法沒有機會被繼承,也沒有機會被重寫。
在設計類的時候,如果這個類不需要有子類,類的實現細節不允許改變,那么就可以設計為final類。
我們在開發中經常使用的String類就是final類,以下為部分源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
......
}
1.2 final修飾方法
在Java中,被final修飾的方法,可以被繼承,但不能被子類重寫(覆蓋)。
在設計方法時,如果這個方法不希望被子類重寫(覆蓋),那么就可以設計為final方法。
舉個具體的例子,我們新建個父類Animal如下:
package com.zwwhnly.springbootaction;
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.springbootaction;
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.springbootaction;
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.3 final修飾成員變量
用final修飾的成員變量沒有默認值,可以在聲明時賦值或者在構造函數中賦值,但必須賦值且只能被賦值1次,賦值后無法修改。
我們修改下1.2中的Cat類代碼,定義2個final成員變量,1個聲明完立即賦值,1個在構造函數中賦值:
package com.zwwhnly.springbootaction;
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.4 final修飾局部變量
被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.5 final修飾方法參數
方法參數其實也是局部變量,因此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.springbootaction;
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.springbootaction;
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.3 try語句塊或者catch語句塊中有return語句
如果try語句塊中有return語句, 是return語句先執行還是finally語句塊先執行呢?
帶着這個問題,我們看下如下這個例子:
package com.zwwhnly.springbootaction;
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.springbootaction;
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.springbootaction;
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.springbootaction;
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.springbootaction;
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.springbootaction;
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.springbootaction;
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 總結
- finally語句塊不一定會被執行
- finally語句塊在 try語句塊中的return 語句之前執行。
- finally語句塊在 catch語句塊中的return 語句之前執行。
- 注意控制轉移語句 return ,break ,continue,throw對執行順序的影響
3. finalize用法
finalize()是Object類的一個方法,因此所有的類都繼承了這個方法。
protected void finalize() throws Throwable { }
finalize()主要用於在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在確定這個對象沒有被引用時對這個對象調用的。
當垃圾回收器(GC)決定回收某對象時,就會運行該對象的finalize()方法。
不過在Java中,如果內存總是充足的,那么垃圾回收可能永遠不會進行,也就是說filalize()可能永遠不被執行,顯然指望它做收尾工作是靠不住的。
4. 參考
原創不易,如果覺得文章能學到東西的話,歡迎點個贊、評個論、關個注,這是我堅持寫作的最大動力。
如果有興趣,歡迎添加我的微信:zwwhnly,等你來聊技術、職場、工作等話題(PS:我是一名奮斗在上海的程序員)。