final、finally、finalize他們三者的差別,是一道再經典不過的面試題,我們在各個公司的面試題中幾乎都能看到它的身影。final、finally和finalize雖然長得像孿生兄弟一樣,但是它們的含義和用法卻是大相徑庭。final是Java中的一個關鍵字,修飾符;finally是Java的一種異常處理機制;finalize是Java中的一個方法名。接下來,我們具體說一下他們三者之間的差別。
一、final
1.1 修飾變量,包含靜态和非靜态
如果final修飾的是一個基本類型,就表示這個變量被賦予的值是不可變的,即它是個常量。
如圖所示final修飾的a,之後再對a修改是無法修改的,會報編譯錯誤。
如果final修飾的是一個對象,就表示這個變量被賦予的引用是不可變的。這裡需要提醒大家注意的是,不可改變的隻是這個變量所儲存的引用,并不是這個引用所指向的對象。
下面我們測試一下,先定義一個Pet實體類
public class Pet {
private String name;
public Pet() {
}
public Pet(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
由上圖測試可見:不可改變的隻是這個變量所儲存的引用,該對象内容還是可以修改的。
當然關注final還有一些細節大家需要注意,如果一個變量或方法參數被final修飾,就表示它隻能被指派一次,但是JAVA虛拟機為變量設定的預設值不記作一次指派。被final修飾的變量必須被初始化。
那接下來我們簡單說一下初始化的幾種情況:
- 在定義的時候初始化
public class User1 {
private final String name = "Lucy";
}
- 非靜态final修飾變量可以在初始化塊中初始化,不可以在靜态初始化塊中初始化;而靜态final修飾的變量可以在靜态初始化塊中初始化,不可以在初始化塊中初始化。
public class User2 {
private final String name;
private static final int a1;
{
name="Lucy";
}
static {
a1=1;
}
//經過代碼測試a2在靜态代碼塊無法進行初始化
/* private final int a2;
static {
a2=1;
}*/
//經過測試,靜态的變量name2不能在初始化代碼塊中初始化
/* private static final String name2;
{
name2="Lucy";
}*/
}
- final變量還可以在類的構造器中初始化,但是靜态final變量不可以。
public class User4 {
private final String name;
//經過測試驗證靜态修飾的a1無法在構造器中進行初始化
private static final int a1;
public User4() {
name="11";
a1=1;
}
}
以上就是初始化的3種情況,大家簡單的認知一下。
那有人會問了:JVM對于聲明為final的局部變量做了那些性能優化呢?在能夠通過編譯的前提下,無論局部變量聲明時帶不帶final關鍵字修飾,對其通路的效率都一樣,我們來進行測試一下:
如下所示不帶final的情況:
static int demo1() {
int a = ValueA();
int b = ValueB();
return a + b;
}
帶final的情況:
static int demo2() {
final int a = ValueA();
final int b = ValueB();
return a + b;
}
他們兩者之間通過Javac編譯後的得到的結果是一樣的:
Code:
0:invokestatic #2 //Method ValueA:()
3:istore_0 // 設定a的值
4:invokestatic #3 //Method ValueB:()
7:istore_1 // 設定b的值
8:iload_0 // 讀取a的值
9:iload_1 // 讀取b的值
10:iadd
11:ireturn
根據位元組碼可見檔案裡除位元組碼外的輔助資料結構也沒有記錄任何展現final的資訊。既然帶不帶final的局部變量在編譯到Class檔案後都一樣了,其通路效率必然一樣高,JVM不可能有辦法知道什麼局部變量原本是用final修飾來聲明的。
當然還是有特殊情況的出現:那就是我們聲明的局部變量并不是一個變量,如下所示的兩種情況:
public class Test01 {
static int foo(){
final int a = 11;
final int b = 12;
return a + b;
}
static int foo1(){
int a = 11;
int b = 12;
return a + b;
}
}
他們兩者之間通過Javac編譯後得到的位元組碼如下所示:
foo()方法中,這樣的話實際上a和b都不是變量,而是編譯時常量,其通路會按照Java語言對常量表達式的規定而做常量折疊。foo1()方法中a和b都是去掉final修飾,那麼a和b就會被看作普通的局部變量而不是常量表達式,是以在位元組碼層面上的效果會不一樣。
其實這種層面上的差異隻對比較簡易的 JVM 影響較大,因為這樣的 JVM 對解釋器的依賴較大,原本 Class 檔案裡的位元組碼是怎樣的它就怎麼執行;對高性能的 JVM則沒啥影響。是以,大部分 final 對性能優化的影響,可以直接忽略,我們使用 final 更多的考量在于其不可變性。
1.2 修飾方法
當定義一個方法被final修飾時,表示此方法不可以被子類重寫,但是依舊可以被子類繼承使用,接下來我們做驗證:
先在父類定義一個final修飾的方法:
public class Fu {
public final void add(){
System.out.println("這是父類final修飾的一個方法");
}
}
子類重寫父類final修飾的方法,并且子類對象調用此方法:
public class ZI extends Fu{
/* public void add(){
System.out.println("子類重寫父類final修飾的方法");
}*/
public static void main(String[] args) {
ZI zi = new ZI();
zi.add();
}
}
經過如上代碼可知,子類重寫父類final修飾的方法會報編譯異常,無法進行重寫此方法,但是子類依舊可以調用此方法。
1.3 修飾類
其實用于final修飾的類我們很熟悉,因為我們最常用的String類就是final修飾的,由于final類不允許被繼承,就意味着它不能再派生出新的子類,不能作為父類被繼承,是以,一個類不能同時被聲明為abstract抽象類和final類。編譯器在處理時把它的所方法都當作final的,是以final類比普通類擁更高的效率。
final的類的所方法都不能被重寫,但這并不表示final的類的屬性(變量值)也是不可改變的,要想做到final類的屬性值不可改變,必須給它增加final修飾。我們進行驗證:
public final class User {
int a1=1;
final int a2 = 2;
public static void main(String[] args) {
User user = new User();
user.a1=3;
// user.a2=3;
System.out.println(user.a1);
}
}
經過如上代碼測試所得,第7行a2被final修飾,不可以二次指派,它是不可變的,而最後的輸出結果也是3,證明a1的值發生了變化。是以final修飾的類并不表示其屬性(變量值)也是不可改變的。
二、finally
finally是什麼呢?是Java的一種異常處理機制。finally隻能用在try/catch語句中,并且是附帶着的一個語句塊,表示這段語句最終總是被執行。請看下面代碼:
public class FinallyTest {
public static void main(String[] args) {
try {
throw new NoSuchFieldException();
} catch (NoSuchFieldException e) {
System.out.println("該程式抛出異常");
} finally {
System.out.println("這裡執行了finally中的代碼");
}
}
}
運作結果如下:
該程式抛出異常
這裡執行了finally中的代碼
由結果可見finally内的代碼被執行了。
那有同學會問,finally的代碼一定會執行嗎?return、continue、break這個可以打亂finally的順序嗎,那接下來我們做一下驗證:
- return是否能影響finally語句塊的執行,看代碼:
public class FinallyTest {
public static void main(String[] args) {
int i = returnTest();
System.out.println("i = " + i);
}
public static int returnTest() {
try {
return 1/0;
} catch (Exception e) {
System.out.println("該程式抛出異常");
} finally {
System.out.println("這裡執行了finally中的代碼");
}
return 1;
}
}
運作結果如下:
該程式抛出異常
這裡執行了finally中的代碼
i = 1
由結果可見,finally内的代碼被執行了,是以return不會影響finally内的代碼。
- continue是否能影響finally語句塊的執行,看代碼:
public class FinallyTest {
public static void main(String[] args) {
continueTest();
}
public static void continueTest() {
for (int i = 0; i < 3; i++) {
try {
if (i==1){
continue;
}
System.out.println("i = " + i);
} catch (Exception e) {
System.out.println("該程式抛出異常");
} finally {
System.out.println("這裡執行了finally中的代碼");
}
}
}
}
運作結果如下:
i = 0
這裡執行了finally中的代碼
這裡執行了finally中的代碼
i = 2
這裡執行了finally中的代碼
由結果所示,當i==1時,條件成立,執行continue,是以i=1的輸出列印被跳出,但是finally内的代碼依舊被執行了,是以continue不會影響finally内的代碼。
- break是否能影響finally語句塊的執行,看代碼:
public class FinallyTest {
public static void main(String[] args) {
continueTest();
}
public static void continueTest() {
for (int i = 0; i < 3; i++) {
try {
if (i==1){
break;
}
System.out.println("i = " + i);
} catch (Exception e) {
System.out.println("該程式抛出異常");
} finally {
System.out.println("這裡執行了finally中的代碼");
}
}
}
}
運作結果如下:
i = 0
這裡執行了finally中的代碼
這裡執行了finally中的代碼
由結果所示,當i==1時,條件成立,執行break,是以後續代碼被終止了,但是i==1時的finally内的代碼依舊被執行了,是以break不會影響finally内的代碼。
顯而易見return、continue、break不會對finally内的代碼造成影響,finally内的代碼一定會被執行。
finally的本質是什麼呢?那我們來看一下,我們就以如下代碼為例:
public class Test01 {
public static void main(String[] args) {
int a1 = 0;
try {
a1 = 1;
}catch (Exception e){
a1 = 2;
}finally {
a1 = 3;
}
System.out.println(a1);
}
}
我們來看一下此段代碼的位元組碼如圖所示:
我們先解釋一下 Exception table,他是一個異常表,其中的每一條都表示一個異常發生器,異常發生器由 From 指針,To 指針,Target 指針和應該捕獲的異常類型構成。
根據如上圖所示的位元組碼我們會發現finally會把“a1=3”的位元組碼 iconst_3 和 istore_1 放在了try塊和catch塊的後面。相當于在try代碼塊和catch的代碼塊的後面都含有一條a1 = 3,是以我們最終的結果一定會執行finally中的代碼。
上面我們讨論的都是 finally 一定會執行的情況,當然值得一提的是finally也不是一定會被執行的。那就是當我們期間調用System.exit()方法,他就會不執行finally的内容,具體如下:
public static void main(String[] args) {
try {
System.out.println(" 執行try中的代碼 " );
System.exit(0);
} catch (Exception e) {
System.out.println("該程式抛出異常");
} finally {
System.out.println("這裡執行了finally中的代碼");
}
}
運作結果如下:
執行try中的代碼
Process finished with exit code 0
由結果顯示,finally内的代碼未執行,System.exit()這個方法是用來結束目前正在運作中的Java虛拟機。對于此方法大家有個印象就可以了,知道該方法會影響到finally的執行。
三、finalize
最後,我們來看一下finalize,他是一個方法,隸屬于java.lang.Object類,方法定義如下:
protected void finalize() throws Throwable { }
此方法是GC運作機制的一部分,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。
在 Java 中,由于 GC 的自動回收機制,因而并不能保證 finalize 方法會被及時地執行(垃圾對象的回收時機具有不确定性),也不能保證它們會被執行。也就是說,finalize 的執行時期不确定,我們并不能依賴于 finalize 方法幫我們進行垃圾回收,可能出現的情況是在我們耗盡資源之前,gc 卻仍未觸發,是以推薦使用資源用完即顯示釋放的方式,比如 close 方法。
finalize 的工作方式是這樣的:一旦垃圾回收器準備好釋放對象占用的存儲空間,将會首先調用 finalize 方法,并且在下一次垃圾回收動作發生時,才會真正回收對象占用的記憶體。
總結
final、finally 和 finalize三者之間看着像孿生兄弟,但是他們三個之間沒有任何的關系。final 是用來修飾類、變量、方法和參數的關鍵字,finally是異常處理語句結構的一部分,表示總是執行。finalize 是 Object 類中的一個基礎方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收的。是以他們之間的差別還是顯而易見的,希望此篇文章幫助讀者更加深刻的區分。