1. 程式人生 > >淺析Java中的final關鍵字

淺析Java中的final關鍵字

談到final關鍵字,想必很多人都不陌生,在使用匿名內部類的時候可能會經常用到final關鍵字。另外,Java中的String類就是一個final類,那麼今天我們就來了解final這個關鍵字的用法。下面是本文的目錄大綱:

一.final關鍵字的基本用法

二.深入理解final關鍵字

若有不正之處,請多多諒解並歡迎指正。

一.final關鍵字的基本用法

在Java中,final關鍵字可以用來修飾類、方法和變數(包括成員變數和區域性變數)。下面就從這三個方面來了解一下final關鍵字的基本用法。

1.修飾類

當用final修飾一個類時,表明這個類不能被繼承。也就是說,如果一個類你永遠不會讓他被繼承,就可以用final進行修飾。final類中的成員變數可以根據需要設為final,但是要注意final類中的所有成員方法都會被隱式地指定為final方法。

在使用final修飾類的時候,要注意謹慎選擇,除非這個類真的在以後不會用來繼承或者出於安全的考慮,儘量不要將類設計為final類。

2.修飾方法

下面這段話摘自《》第四版第143頁:

“使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。在早期的Java實現版本中,會將final方法轉為內嵌呼叫。但是如果方法過於龐大,可能看不到內嵌呼叫帶來的任何效能提升。在最近的Java版本中,不需要使用final方法進行這些優化了。“

因此,如果只有在想明確禁止 該方法在子類中被覆蓋的情況下才將方法設定為final的。

注:類的private方法會隱式地被指定為final方法

3.修飾變數

修飾變數是final用得最多的地方,也是本文接下來要重點闡述的內容。首先了解一下final變數的基本語法:

對於一個final變數,如果是基本資料型別的變數,則其數值一旦在初始化之後便不能更改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。

舉個例子:

上面的一段程式碼中,對變數i和obj的重新賦值都報錯了。

二.深入理解final關鍵字

在瞭解了final關鍵字的基本用法之後,這一節我們來看一下final關鍵字容易混淆的地方。

1.類的final變數和普通變數有什麼區別?

當用final作用於類的成員變數時,成員變數(注意是類的成員變數,區域性變數只需要保證在使用之前被初始化賦值即可)必須在定義時或者構造器中進行初始化賦值,而且final變數一旦被初始化賦值之後,就不能再被賦值了。

那麼final變數和普通變數到底有何區別呢?下面請看一個例子:

public class Test {
    public static void main(String[] args)  {
        String a = "hello2"; 
        final String b = "hello";
        String d = "hello";
        String c = b + 2; 
        String e = d + 2;
        System.out.println((a == c));
        System.out.println((a == e));
    }
}
true
false

大家可以先想一下這道題的輸出結果。為什麼第一個比較結果為true,而第二個比較結果為fasle。這裡面就是final變數和普通變數的區別了,當final變數是基本資料型別以及String型別時,如果在編譯期間能知道它的確切值,則編譯器會把它當做編譯期常量使用。也就是說在用到該final變數的地方,相當於直接訪問的這個常量,不需要在執行時確定。這種和C語言中的巨集替換有點像。因此在上面的一段程式碼中,由於變數b被final修飾,因此會被當做編譯器常量,所以在使用到b的地方會直接將變數b 替換為它的  值。而對於變數d的訪問卻需要在執行時通過連結來進行。想必其中的區別大家應該明白了,不過要注意,只有在編譯期間能確切知道final變數值的情況下,編譯器才會進行這樣的優化,比如下面的這段程式碼就不會進行優化:

public class Test {
    public static void main(String[] args)  {
        String a = "hello2"; 
        final String b = getHello();
        String c = b + 2; 
        System.out.println((a == c));

    }

    public static String getHello() {
        return "hello";
    }
}

這段程式碼的輸出結果為false。

2.被final修飾的引用變數指向的物件內容可變嗎?

在上面提到被final修飾的引用變數一旦初始化賦值之後就不能再指向其他的物件,那麼該引用變數指向的物件的內容可變嗎?看下面這個例子:

public class Test {
    public static void main(String[] args)  {
        final MyClass myClass = new MyClass();
        System.out.println(++myClass.i);

    }
}

class MyClass {
    public int i = 0;
}

這段程式碼可以順利編譯通過並且有輸出結果,輸出結果為1。這說明引用變數被final修飾之後,雖然不能再指向其他物件,但是它指向的物件的內容是可變的。

3.final和static

很多時候會容易把static和final關鍵字混淆,static作用於成員變數用來表示只儲存一份副本,而final的作用是用來保證變數不可變。看下面這個例子:

public class Test {
    public static void main(String[] args)  {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();
        System.out.println(myClass1.i);
        System.out.println(myClass1.j);
        System.out.println(myClass2.i);
        System.out.println(myClass2.j);

    }
}

class MyClass {
    public final double i = Math.random();
    public static double j = Math.random();
}

執行這段程式碼就會發現,每次列印的兩個j值都是一樣的,而i的值卻是不同的。從這裡就可以知道final和static變數的區別了。

4.匿名內部類中使用的外部區域性變數為什麼只能是final變數?

這個問題請參見上一篇博文中《Java內部類詳解》中的解釋,在此處不再贅述。

5.關於final引數的問題

關於網上流傳的”當你在方法中不需要改變作為引數的物件變數時,明確使用final進行宣告,會防止你無意的修改而影響到呼叫方法外的變數“這句話,我個人理解這樣說是不恰當的。

因為無論引數是基本資料型別的變數還是引用型別的變數,使用final宣告都不會達到上面所說的效果。

看這個例子就清楚了:

上面這段程式碼好像讓人覺得用final修飾之後,就不能在方法中更改變數i的值了。殊不知,方法changeValue和main方法中的變數i根本就不是一個變數,因為java引數傳遞採用的是值傳遞,對於基本型別的變數,相當於直接將變數進行了拷貝。所以即使沒有final修飾的情況下,在方法內部改變了變數i的值也不會影響方法外的i。

再看下面這段程式碼:

public class Test {
    public static void main(String[] args)  {
        MyClass myClass = new MyClass();
        StringBuffer buffer = new StringBuffer("hello");
        myClass.changeValue(buffer);
        System.out.println(buffer.toString());
    }
}

class MyClass {

    void changeValue(final StringBuffer buffer) {
        buffer.append("world");
    }
}

執行這段程式碼就會發現輸出結果為 helloworld。很顯然,用final進行修飾並沒有阻止在changeValue中改變buffer指向的物件的內容。有人說假如把final去掉了,萬一在changeValue中讓buffer指向了其他物件怎麼辦。有這種想法的朋友可以自己動手寫程式碼試一下這樣的結果是什麼,如果把final去掉了,然後在changeValue中讓buffer指向了其他物件,也不會影響到main方法中的buffer,原因在於java採用的是值傳遞,對於引用變數,傳遞的是引用的值,也就是說讓實參和形參同時指向了同一個物件,因此讓形參重新指向另一個物件對實參並沒有任何影響。

所以關於網上流傳的final引數的說法,我個人不是很贊同。

參考資料:

《Java程式設計思想》