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