一、摘要
?我們大家都知道,Java中平時(shí)用的比較多的String類型是不可以被繼承的,因?yàn)镾tring類有final修飾,來看下String類的定義:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
?很明顯有一個(gè)final的關(guān)鍵字,那么在Java中,final有哪些作用?到底怎么實(shí)現(xiàn)的呢?內(nèi)存語義是啥呢?接下來的文章將重點(diǎn)分析final的作用以及內(nèi)存語義等等。
二、final關(guān)鍵字的作用
?根據(jù)上下文環(huán)境,Java的關(guān)鍵字final的含義存在著細(xì)微的區(qū)別,但通常它指的是“這是無法改變的?!辈幌胱龈淖兛赡艹鲇趦煞N理由:設(shè)計(jì)或效率。由于這兩個(gè)原因相差很遠(yuǎn),所以關(guān)鍵字final有可能被誤用。
?基于Java語言規(guī)范,我們知道final可以修飾變量、方法以及類,接下來我們分別介紹final修飾它們的作用。
2.1 final修飾變量
?先來看下Java語言規(guī)范中關(guān)于final修飾變量的描述:
?變量可以被聲明為final,而final變量只能被賦值一次。如果對(duì)final變量賦值,那么除非在賦值之前該變量是明確未賦值的,否則就是一種編譯時(shí)錯(cuò)誤。
?一旦final變量被賦值,那么它就始終有同一個(gè)值。如果一個(gè)final變量持有的是對(duì)象的引用,那么該對(duì)象的狀態(tài)可以被對(duì)象上的操作所修改,但是該變量會(huì)始終指向這個(gè)對(duì)象。這條規(guī)則也同樣適用于數(shù)組,因?yàn)閿?shù)組也是對(duì)象。如果一個(gè)final變量持有的是指向數(shù)組的引用,那么該數(shù)組的元素可以被數(shù)組上的操作所修改,但是該變量會(huì)始終指向這個(gè)數(shù)組。
?空final是指其聲明缺少初始化器的final變量。
?常量變量是指用常量表達(dá)式初始化的簡單類型或String類型的final變量。
?有三種變量被隱式地聲明為final:接口的域、帶資源的try語句中的資源,以及多重catch子句中的異常參數(shù)。單catch子句的異常參數(shù)永遠(yuǎn)都不會(huì)被隱式地聲明為final,但是它可以被認(rèn)為效果等同于final。
?從規(guī)范中,可以看出來final變量只能被賦值一次;而變量我們分為基本數(shù)據(jù)類型和引用類型的變量,對(duì)于基本數(shù)據(jù)類型的變量:final使數(shù)值恒定不變;對(duì)于引用類型的final變量:final使引用恒定不變。一旦引用被初始化指向一個(gè)對(duì)象,就無法再把它改為指向另一個(gè)對(duì)象。然后,對(duì)象自身卻是可以被修改的,Java并未提供使任何對(duì)象恒定不變的途徑(但可以自己編寫類以取得對(duì)象恒定不變的效果)。這一限制同樣適用于數(shù)組,它也是對(duì)象。
?一個(gè)既是static又是final的域只占據(jù)一段不能改變的存儲(chǔ)空間。
?下面的例子示范了final域的情況。
import java.util.Random;
class Value {
int i; // Package access
public Value(int i) { this.i = i; }
}
public class FinalData {
private static Random rand = new Random(47);
private String id;
public FinalData(String id) {
this.id = id;
}
// Can be compile-time constants:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// Typical public constant:
public static final int VALUE_THREE = 39;
// Cannot be compile-time constants:
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
// Arrays:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(9); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(0); // Error: Can't
//! fd1.VAL_3 = new Value(1); // change reference
//! fd1.a = new int[3];
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~
?由于valueOne和VAL_TWO都是帶編譯時(shí)數(shù)值的final基本類型,所以它們二者均可以用作編譯器常量,并且沒有特別大的區(qū)別。VAL_THREE是一種更加典型的對(duì)常量進(jìn)行定義的方式:定義為public,則可以被用于包之外;定義為static,則強(qiáng)調(diào)只有一份;定義為final,則說明它是一個(gè)常量。請(qǐng)注意,帶有恒定初始值(即編譯器常量)的final static基本類型全用大寫字母命名,并且字與字之間用下劃線隔開。
?我們不能因?yàn)槟硵?shù)據(jù)是final的就認(rèn)為在編譯時(shí)可以知道它的值。在運(yùn)行時(shí)使用隨機(jī)生成的數(shù)值來初始化i4和INT_5就說明了這一點(diǎn)。示例部分也展示了將final數(shù)值定義為靜態(tài)和費(fèi)靜態(tài)的區(qū)別。此區(qū)別只有當(dāng)數(shù)值在運(yùn)行時(shí)被初始化時(shí)才會(huì)顯現(xiàn),這是因?yàn)榫幾g器對(duì)編譯時(shí)數(shù)值一視同仁。當(dāng)運(yùn)行程序時(shí)就會(huì)看到這個(gè)區(qū)別。注意,在fd1和fd2中,i4的值是唯一的,但I(xiàn)NT_5的值是不可以通過創(chuàng)建第二個(gè)FinalData對(duì)象而加以改變的。這是因?yàn)樗莝tatic的,在裝載時(shí)已被初始化,而不是每次創(chuàng)建新對(duì)象時(shí)都初始化。
?v1到VAL_3這些變量說明了final引用的意義。正如在main()中所看到的,不能因?yàn)関2是final的,就認(rèn)為無法改變它的值。由于它是一個(gè)引用,final意味著無法將v2再次指向另一個(gè)新的對(duì)象。
2.2 空白final
?Java允許生成“空白final”,所謂空白final是指被聲明為final但又未給定初始值的域。無論什么情況,編譯器都確??瞻譮inal在使用前必須被初始化。但是,空白final在關(guān)鍵字final的使用上提供了更大的靈活性,為此,一個(gè)類中的final域就可以做到根據(jù)對(duì)象而有所不同,卻又保持其恒定不變的特性。例如:
class Poppet {
private int i;
Poppet(int ii) {
i = ii;
}
public int getI() {
return i;
}
}
public class BlankFinal {
private final int i = 0; // Initialized final
private final int j; // Blank final
private final Poppet p; // Blank final reference
// Blank finals MUST be initialized in the constructor:
public BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet(1); // Initialize blank final reference
}
public BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet(x); // Initialize blank final reference
}
public static void main(String[] args) {
BlankFinal bf1 = new BlankFinal();
BlankFinal bf2 = new BlankFinal(47);
System.out.println(bf1.p.getI());
System.out.println(bf2.p.getI());
}
} /* Output:
1
47
*///:~
?必須在域的定義處或者每個(gè)構(gòu)造器中用表達(dá)式對(duì)final進(jìn)行賦值,這正是final域在使用前總是被初始化的原因所在。
2.3 final參數(shù)
?Java允許在參數(shù)列表中以聲明的方式將參數(shù)知名為final。這意味著你無法再方法中更改參數(shù)引用所指向的對(duì)象:
class Gizmo {
public void spin() {
}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) {
return i + 1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
} ///:~
?方法f()和g()展示了當(dāng)基本類型的參數(shù)被指明為final時(shí)所出現(xiàn)的結(jié)果:你可以讀參數(shù),但無法修改參數(shù)。這一特性主要用來想匿名內(nèi)部類傳遞數(shù)據(jù)。
2.4 final方法
?使用final方法的原因有兩個(gè)。第一個(gè)原因是把方法鎖定,以防任何繼承類修改它的含義。這是出于設(shè)計(jì)的考慮:想要確保在繼承中使方法行為保持不變,并且不會(huì)被覆蓋。
?過去建議使用final方法的第二個(gè)原因是效率,在Java的早期實(shí)現(xiàn)中,如果將一個(gè)方法指明為final,就是同意編譯器將針對(duì)該方法的所有調(diào)用都轉(zhuǎn)為內(nèi)聯(lián)調(diào)用。當(dāng)編譯器發(fā)現(xiàn)一個(gè)final方法調(diào)用命令時(shí),它會(huì)根據(jù)自己的謹(jǐn)慎判斷,跳過插入程序代碼這種正常方式而執(zhí)行方法調(diào)用機(jī)制,并且以方法體重的時(shí)機(jī)代碼的副本來替代方法調(diào)用。這將消除方法調(diào)用的開銷。當(dāng)然,如果一個(gè)方法很大,你的程序代碼就會(huì)膨脹,因而可能看不到內(nèi)聯(lián)帶來的任何性能提高,因?yàn)?,所帶來的性能提高?huì)因?yàn)榛ㄙM(fèi)于方法內(nèi)的時(shí)間量而被縮減。
?在最新的Java版本中,虛擬機(jī)(特別是hotspot技術(shù))可以探測(cè)到這些情況,并優(yōu)化去掉這些效率反而降低的額外的內(nèi)聯(lián)調(diào)用,因此不再需要使用final方法來進(jìn)行優(yōu)化了。在使用新的Java版本時(shí),應(yīng)該讓編譯器和JVM去處理效率問題,只有在想要明確禁止覆蓋時(shí),才將方法設(shè)置為final的。
fina和private關(guān)鍵字
?類中所有的private方法都隱式地指定為是final的。由于無法取用private方法,所以也就無法覆蓋它??梢詫?duì)private方法添加final修飾詞,但這并不給該方法增加任何額外的意義。
2.5 final類
?當(dāng)將某個(gè)類的整體定義為final時(shí)(通過將關(guān)鍵字final置于它的定義之前),就表明了你不打算繼承該類,而且也不允許別人這樣做。換句話說,出于某種考慮,你對(duì)該類的設(shè)計(jì)用不需要做任何變動(dòng),或者處于安全的考慮,你不希望它有子類。
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
} ///:~
?請(qǐng)注意,final類的域可以根據(jù)個(gè)人的意愿選擇為是或不是final。不論類是否被定義為final,相同的規(guī)則都適用于定義為final的域。然而,由于final類禁止繼承,所以final類中所有的方法都隱式指定為是final的,因?yàn)闊o法覆蓋它們。在final類中可以給方法添加final修飾詞,但這不會(huì)增添任何意義。
三、final的原理
四、參考引用
Bruce Eckel 《Java編程思想(第四版)》