談到final關(guān)鍵字,想必很多人都不陌生,在使用匿名內(nèi)部類的時候可能會經(jīng)常用到final關(guān)鍵字。另外,Java中的String類就是一個final類,那么今天我們就來了解final這個關(guān)鍵字的用法。
一、深入理解final關(guān)鍵字
在了解了final關(guān)鍵字的基本用法之后,這一節(jié)我們來看一下final關(guān)鍵字容易混淆的地方。
1.類的final變量和普通變量有什么區(qū)別?
當(dāng)用final作用于類的成員變量時,成員變量(注意是類的成員變量,局部變量只需要保證在使用之前被初始化賦值即可)必須在定義時或者構(gòu)造器中進行初始化賦值,而且final變量一旦被初始化賦值之后,就不能再被賦值了。
那么final變量和普通變量到底有何區(qū)別呢?下面請看一個例子:
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));
}
}
View Code
大家可以先想一下這道題的輸出結(jié)果。為什么第一個比較結(jié)果為true,而第二個比較結(jié)果為fasle。這里面就是final變量和普通變量的區(qū)別了,當(dāng)final變量是基本數(shù)據(jù)類型以及String類型時,如果在編譯期間能知道它的確切值,則編譯器會把它當(dāng)做編譯期常量使用。也就是說在用到該final變量的地方,相當(dāng)于直接訪問的這個常量,不需要在運行時確定。這種和C語言中的宏替換有點像。因此在上面的一段代碼中,由于變量b被final修飾,因此會被當(dāng)做編譯器常量,所以在使用到b的地方會直接將變量b 替換為它的 值。而對于變量d的訪問卻需要在運行時通過鏈接來進行。想必其中的區(qū)別大家應(yīng)該明白了,不過要注意,只有在編譯期間能確切知道final變量值的情況下,編譯器才會進行這樣的優(yōu)化,比如下面的這段代碼就不會進行優(yōu)化:
13public 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”;
}
}
這段代碼的輸出結(jié)果為false。
2.被final修飾的引用變量指向的對象內(nèi)容可變嗎?
在上面提到被final修飾的引用變量一旦初始化賦值之后就不能再指向其他的對象,那么該引用變量指向的對象的內(nèi)容可變嗎?看下面這個例子:
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;
}
這段代碼可以順利編譯通過并且有輸出結(jié)果,輸出結(jié)果為1。這說明引用變量被final修飾之后,雖然不能再指向其他對象,但是它指向的對象的內(nèi)容是可變的。
3.final和static
很多時候會容易把static和final關(guān)鍵字混淆,static作用于成員變量用來表示只保存一份副本,而final的作用是用來保證變量不可變。看下面這個例子:
16public 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();
}
運行這段代碼就會發(fā)現(xiàn),每次打印的兩個j值都是一樣的,而i的值卻是不同的。從這里就可以知道final和static變量的區(qū)別了。
4.匿名內(nèi)部類中使用的外部局部變量為什么只能是final變量?
這個問題請參見上一篇博文中《Java內(nèi)部類詳解》中的解釋,在此處不再贅述。
5.關(guān)于final參數(shù)的問題
關(guān)于網(wǎng)上流傳的”當(dāng)你在方法中不需要改變作為參數(shù)的對象變量時,明確使用final進行聲明,會防止你無意的修改而影響到調(diào)用方法外的變量“這句話,我個人理解這樣說是不恰當(dāng)?shù)摹?/p>
因為無論參數(shù)是基本數(shù)據(jù)類型的變量還是引用類型的變量,使用final聲明都不會達到上面所說的效果。
看這個例子就清楚了:
上面這段代碼好像讓人覺得用final修飾之后,就不能在方法中更改變量i的值了。殊不知,方法changeValue和main方法中的變量i根本就不是一個變量,因為java參數(shù)傳遞采用的是值傳遞,對于基本類型的變量,相當(dāng)于直接將變量進行了拷貝。所以即使沒有final修飾的情況下,在方法內(nèi)部改變了變量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”);
}
}
運行這段代碼就會發(fā)現(xiàn)輸出結(jié)果為 helloworld。很顯然,用final進行修飾并沒有阻止在changeValue中改變buffer指向的對象的內(nèi)容。有人說假如把final去掉了,萬一在changeValue中讓buffer指向了其他對象怎么辦。有這種想法的朋友可以自己動手寫代碼試一下這樣的結(jié)果是什么,如果把final去掉了,然后在changeValue中讓buffer指向了其他對象,也不會影響到main方法中的buffer,原因在于java采用的是值傳遞,對于引用變量,傳遞的是引用的值,也就是說讓實參和形參同時指向了同一個對象,因此讓形參重新指向另一個對象對實參并沒有任何影響。
二、final關(guān)鍵字的幾種用法
1.修飾數(shù)據(jù)
在編寫程序時,我們經(jīng)常需要說明一個數(shù)據(jù)是不可變的,我們成為常量。在java中,用final關(guān)鍵字修飾的變量,只能進行一次賦值操作,并且在生存期內(nèi)不可以改變它的值。更重要的是,final會告訴編譯器,這個數(shù)據(jù)是不會修改的,那么編譯器就可能會在編譯時期就對該數(shù)據(jù)進行替換甚至執(zhí)行計算,這樣可以對我們的程序起到一點優(yōu)化。不過在針對基本類型和引用類型時,final關(guān)鍵字的效果存在細微差別。我們來看下面的例子:
1 class Value {
2 int v;
3 public Value(int v) {
4 this.v = v;
5 }
6 }
7
8 public class FinalTest {
9
10 final int f1 = 1;
11 final int f2;
12 public FinalTest() {
13 f2 = 2;
14 }
15
16 public static void main(String[] args) {
17 final int value1 = 1;
18 // value1 = 4;
19 final double value2;
20 value2 = 2.0;
21 final Value value3 = new Value(1);
22 value3.v = 4;
23 }
24 }
上面的例子中,我們先來看一下main方法中的幾個final修飾的數(shù)據(jù),在給value1賦初始值之后,我們無法再對value1的值進行修改,final關(guān)鍵字起到了常量的作用。從value2我們可以看到,final修飾的變量可以不在聲明時賦值,即可以先聲明,后賦值。value3時一個引用變量,這里我們可以看到final修飾引用變量時,只是限定了引用變量的引用不可改變,即不能將value3再次引用另一個Value對象,但是引用的對象的值是可以改變的,從內(nèi)存模型中我們看的更加清晰:
上圖中,final修飾的值用粗線條的邊框表示它的值是不可改變的,我們知道引用變量的值實際上是它所引用的對象的地址,也就是說該地址的值是不可改變的,從而說明了為什么引用變量不可以改變引用對象。而實際引用的對象實際上是不受final關(guān)鍵字的影響的,所以它的值是可以改變的。
另一方面,我們看到了用final修飾成員變量時的細微差別,因為final修飾的數(shù)據(jù)的值是不可改變的,所以我們必須確保在使用前就已經(jīng)對成員變量賦值了。因此對于final修飾的成員變量,我們有且只有兩個地方可以給它賦值,一個是聲明該成員時賦值,另一個是在構(gòu)造方法中賦值,在這兩個地方我們必須給它們賦初始值。
最后我們需要注意的一點是,同時使用static和final修飾的成員在內(nèi)存中只占據(jù)一段不能改變的存儲空間。
2.修飾方法參數(shù)
前面我們可以看到,如果變量是我們自己創(chuàng)建的,那么使用final修飾表示我們只會給它賦值一次且不會改變變量的值。那么如果變量是作為參數(shù)傳入的,我們怎么保證它的值不會改變呢?這就用到了final的第二種用法,即在我們編寫方法時,可以在參數(shù)前面添加final關(guān)鍵字,它表示在整個方法中,我們不會(實際上是不能)改變參數(shù)的值:
public class FinalTest {
/* 。。。 */
public void finalFunc(final int i, final Value value) {
// i = 5; 不能改變i的值
// v = new Value(); 不能改變v的值
value.v = 5; // 可以改變引用對象的值
}
}
3.修飾方法
第三種方式,即用final關(guān)鍵字修飾方法,它表示該方法不能被覆蓋。這種使用方式主要是從設(shè)計的角度考慮,即明確告訴其他可能會繼承該類的程序員,不希望他們?nèi)ジ采w這個方法。這種方式我們很容易理解,然而,關(guān)于private和final關(guān)鍵字還有一點聯(lián)系,這就是類中所有的private方法都隱式地指定為是final的,由于無法在類外使用private方法,所以也就無法覆蓋它。
4.修飾類
了解了final關(guān)鍵字的其他用法,我們很容易可以想到使用final關(guān)鍵字修飾類的作用,那就是用final修飾的類是無法被繼承的。
上面我們講解了final的四種用法,然而,對于第三種和第四種用法,我們卻甚少使用。這不是沒有道理的,從final的設(shè)計來講,這兩種用法甚至可以說是雞肋,因為對于開發(fā)人員來講,如果我們寫的類被繼承的越多,就說明我們寫的類越有價值,越成功。即使是從設(shè)計的角度來講,也沒有必要將一個類設(shè)計為不可繼承的。Java標(biāo)準(zhǔn)庫就是一個很好的反例,特別是Java 1.0/1.1中Vector類被如此廣泛的運用,如果所有的方法均未被指定為final的話,它可能會更加有用。如此有用的類,我們很容易想到去繼承和重寫他們,然而,由于final的作用,導(dǎo)致我們對Vector類的擴展受到了一些阻礙,導(dǎo)致了Vector并沒有完全發(fā)揮它應(yīng)有的全部價值。
評論
查看更多