一、前言
?
? ? ? ?在java的關鍵字中,static和final是兩個我們必須掌握的關鍵字。不同于其他關鍵字,他們都有多種用法,而且在一定環境下使用,可以提高程序的運行性能,優化程序的結構。下面我們來了解一下final關鍵字及其用法。
?
二、final關鍵字
?
? ? ? ?在java中,final的含義在不同的場景下有細微的差別,但總體上來說,它指的是“這是不可變的”。不想被改變的原因有兩個:效率、設計。使用到final的有三種情況:數據、方法、類。下面,我們來講final的四種主要用法。
?
1、修飾變量
? ? ? ?有時候數據的恒定不變是很有用的,它能夠減輕系統運行時的負擔。對于這些恒定不變的數據我可以叫做“常量”。在java中,用final關鍵字修飾的變量,只能進行一次賦值操作,并且在生存期內不可以改變它的值。“常量”主要應用與以下兩個地方:
? ? ? ?1、編譯期常量,永遠不可改變。
? ? ? ?2、運行期初始化時,我們希望它不會被改變。
? ? ? ?對于編譯期常量,它在類加載的過程就已經完成了初始化,所以當類加載完成后是不可更改的,編譯期可以將它代入到任何用到它的計算式中,也就是說可以在編譯期執行計算式。當然對于編譯期常量,只能使用基本類型,而且必須要在定義時進行初始化。
? ? ? ?有些變量,我們希望它可以根據對象的不同而表現不同,但同時又不希望它被改變,這個時候我們就可以使用運行期常量。對于運行期常量,它既可是基本數據類型,也可是引用數據類型。基本數據類型不可變的是其內容,而引用數據類型不可變的是其引用,引用所指定的對象內容是可變的。不過在針對基本類型和引用類型時,final關鍵字的效果存在細微差別。我們來看下面的例子:
class Value {int v;public Value(int v) {this.v = v;}
}public class FinalTest {final int f1 = 1;final int f2;public FinalTest() {f2 = 2;}public static void main(String[] args) {final int value1 = 1;// value1 = 4;final double value2;value2 = 2.0;final Value value3 = new Value(1);value3.v = 4;}
}
? ? ? ?上面的例子中,我們先來看一下main方法中的幾個final修飾的數據,在給value1賦初始值之后,我們無法再對value1的值進行修改,final關鍵字起到了常量的作用。從value2我們可以看到,final修飾的變量可以不在聲明時賦值,即可以先聲明,后賦值。value3時一個引用變量,這里我們可以看到final修飾引用變量時,只是限定了引用變量的引用不可改變,即不能將value3再次引用另一個Value對象,但是引用的對象的值是可以改變的,從內存模型中我們看的更加清晰:
上圖中,final修飾的值用粗線條的邊框表示它的值是不可改變的,我們知道引用變量的值實際上是它所引用的對象的地址,也就是說該地址的值是不可改變的,從而說明了為什么引用變量不可以改變引用對象。而實際引用的對象實際上是不受final關鍵字的影響的,所以它的值是可以改變的。
另一方面,我們看到了用final修飾成員變量時的細微差別,因為final修飾的數據的值是不可改變的,所以我們必須確保在使用前就已經對成員變量賦值了。因此對于final修飾的成員變量,我們有且只有兩個地方可以給它賦值,一個是聲明該成員時賦值,另一個是在構造方法中賦值,在這兩個地方我們必須給它們賦初始值。
最后我們需要注意的一點是,同時使用static和final修飾的成員在內存中只占據一段不能改變的存儲空間。
?
2、修飾方法參數
? ? ? ?前面我們可以看到,如果變量是我們自己創建的,那么使用final修飾表示我們只會給它賦值一次且不會改變變量的值。那么如果變量是作為參數傳入的,我們怎么保證它的值不會改變呢?這就用到了final的第二種用法,即在我們編寫方法時,可以在參數前面添加final關鍵字,它表示在整個方法中,我們不會(實際上是不能)改變參數的值:
public class FinalTest {/* ... */public void finalFunc(final int i, final Value value) {// i = 5; 不能改變i的值// v = new Value(); 不能改變v的值value.v = 5; // 可以改變引用對象的值}
}
?
3、修飾方法
? ? ? ?第三種方式,即用final關鍵字修飾方法,它表示該方法不能被覆蓋。這種使用方式主要是從設計的角度考慮,即明確告訴其他可能會繼承該類的程序員,不希望他們去覆蓋這個方法。這種方式我們很容易理解。
下面這段話摘自《Java編程思想》第四版第143頁:
? ? ? ?“使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。在java的早期實現中,如果將一個方法指明為final,就是同意編譯器將針對該方法的所有調用都轉為內嵌調用。當編譯器發現一個final方法調用命令時,它會根據自己的謹慎判斷,跳過插入程序代碼這種正常的調用方式而執行方法調用機制(將參數壓入棧,跳至方法代碼處執行,然后跳回并清理棧中的參數,處理返回值),并且以方法體中的實際代碼的副本來代替方法調用。這將消除方法調用的開銷。當然,如果一個方法很大,你的程序代碼會膨脹,因而可能看不到內嵌所帶來的性能上的提高,因為所帶來的性能會花費于方法內的時間量而被縮減。在最近的Java版本中,不需要使用final方法進行這些優化了。“
因此,如果只有在想明確禁止該方法在子類中被覆蓋的情況下才將方法設置為final的。
? ? ? ?注:類的private方法會隱式地被指定為final方法。
?
4、修飾類
? ? ? ?當用final修飾一個類時,表明這個類不能被繼承。也就是說,如果一個類你永遠不會讓他被繼承,就可以用final進行修飾。final類中的成員變量可以根據需要設為final,但是要注意final類中的所有成員方法都會被隱式地指定為final方法。
? ? ? ?注:在使用final修飾類的時候,要注意謹慎選擇,除非這個類真的在以后不會用來繼承或者出于安全的考慮,盡量不要將類設計為final類。
?
? ? ? ?上面我們講解了final的四種用法,然而,對于第三種和第四種用法,我們卻甚少使用。這不是沒有道理的,從final的設計來講,這兩種用法甚至可以說是雞肋,因為對于開發人員來講,如果我們寫的類被繼承的越多,就說明我們寫的類越有價值,越成功。即使是從設計的角度來講,也沒有必要將一個類設計為不可繼承的。Java標準庫就是一個很好的反例,特別是Java 1.0/1.1中Vector類被如此廣泛的運用,如果所有的方法均未被指定為final的話,它可能會更加有用。如此有用的類,我們很容易想到去繼承和重寫他們,然而,由于final的作用,導致我們對Vector類的擴展受到了一些阻礙,導致了Vector并沒有完全發揮它應有的全部價值。
?
三、深入理解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));}/*** Output:* 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";}/*** Output:* false*/
}
? ? ? ?這段代碼的輸出結果為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();
}
? ? ? ?運行這段代碼就會發現,每次打印的兩個i值都是一樣的,而j的值卻是不同的。從這里就可以知道final和static變量的區別了。
?
?
?
?