本文原文地址:https://jiang-hao.com/articles/2019/coding-java-final-keyword.html[1]
final 簡介[2]
final關(guān)鍵字可用于多個場景,且在不同場景具有不同的作用。首先,final是一個非訪問修飾符,僅適用于變量,方法或類。下面是使用final的不同場景:
[圖片上傳失敗...(image-bee40-1555334312011)]
上面這張圖可以概括成:
- 當(dāng)final修飾變量時,被修飾的變量必須被初始化(賦值),且后續(xù)不能修改其值,實質(zhì)上是常量;
- 當(dāng)final修飾方法時,被修飾的方法無法被所在類的子類重寫(覆寫);
- 當(dāng)final修飾類時,被修飾的類不能被繼承,并且final類中的所有成員方法都會被隱式地指定為final方法,但成員變量則不會變。
final 修飾變量
當(dāng)使用final關(guān)鍵字聲明類成員變量或局部變量后,其值不能被再次修改;也經(jīng)常和static關(guān)鍵字一起,作為類常量使用。很多時候會容易把static和final關(guān)鍵字混淆,<u>static作用于成員變量用來表示只保存一份副本,而final的作用是用來保證變量不可變</u>。如果final變量是引用,這意味著該變量不能重新綁定到引用另一個對象,但是可以更改該引用變量指向的對象的內(nèi)部狀態(tài),即可以從final數(shù)組或final集合中添加或刪除元素。最好用全部大寫來表示final變量,使用下劃線來分隔單詞。
例子:
//一個final成員常量
final int THRESHOLD = 5;
//一個空的final成員常量
final int THRESHOLD;
//一個靜態(tài)final類常量
static final double PI = 3.141592653589793;
//一個空的靜態(tài)final類常量
static final double PI;
初始化final變量:
我們必須初始化一個final變量,否則編譯器將拋出編譯時錯誤。final變量只能通過初始化器或賦值語句初始化一次。初始化final變量有三種方法:
- 可以在聲明它時初始化final變量。這種方法是最常見的。如果在聲明時未初始化,則該變量稱為空final變量。下面是初始化空final變量的兩種方法。
- 可以在instance-initializer塊 或內(nèi)部構(gòu)造函數(shù)中初始化空的final變量。如果您的類中有多個構(gòu)造函數(shù),則必須在所有構(gòu)造函數(shù)中初始化它,否則將拋出編譯時錯誤。
- 可以在靜態(tài)塊內(nèi)初始化空的final靜態(tài)變量。
這里注意有一個很普遍的誤區(qū)。<u>很多人會認(rèn)為static修飾的final常量必須在聲明時就進(jìn)行初始化,否則會報錯。但其實則不然,我們可以先使用static final關(guān)鍵字聲明一個類常量,然后再在靜態(tài)塊內(nèi)初始化空的final靜態(tài)變量。</u>讓我們通過一個例子看上面初始化final變量的不同方法。
// Java program to demonstrate different
// ways of initializing a final variable
class Gfg
{
// a final variable direct initialize
// 直接賦值
final int THRESHOLD = 5;
// a blank final variable
// 空final變量
final int CAPACITY;
// another blank final variable
final int MINIMUM;
// a final static variable PI direct initialize
// 直接賦值的靜態(tài)final變量
static final double PI = 3.141592653589793;
// a blank final static variable
// 空的靜態(tài)final變量,此處并不會報錯,因為在下方的靜態(tài)代碼塊內(nèi)對其進(jìn)行了初始化
static final double EULERCONSTANT;
// instance initializer block for initializing CAPACITY
// 用來賦值空final變量的實例初始化塊
{
CAPACITY = 25;
}
// static initializer block for initializing EULERCONSTANT
// 用來賦值空final變量的靜態(tài)初始化塊
static{
EULERCONSTANT = 2.3;
}
// constructor for initializing MINIMUM
// Note that if there are more than one
// constructor, you must initialize MINIMUM
// in them also
// 構(gòu)造函數(shù)內(nèi)初始化空final變量;注意如果有多個
// 構(gòu)造函數(shù)時,必須在每個中都初始化該final變量
public GFG()
{
MINIMUM = -1;
}
}
何時使用final變量:**
普通變量和final變量之間的唯一區(qū)別是我們可以將值重新賦值給普通變量;但是對于final變量,一旦賦值,我們就不能改變final變量的值。因此,final變量必須僅用于我們希望在整個程序執(zhí)行期間保持不變的值。
final引用變量:
當(dāng)final變量是對象的引用時,則此變量稱為final引用變量。例如,final的StringBuffer變量:
final StringBuffer sb;
final變量無法重新賦值。但是對于final的引用變量,可以更改該引用變量指向的對象的內(nèi)部狀態(tài)。請注意,這不是重新賦值。final的這個屬性稱為非傳遞性。要了解對象內(nèi)部狀態(tài)的含義,請參閱下面的示例:
// Java program to demonstrate
// reference final variable
class Gfg
{
public static void main(String[] args)
{
// a final reference variable sb
final StringBuilder sb = new StringBuilder("Geeks");
System.out.println(sb);
// changing internal state of object
// reference by final reference variable sb
// 更改final變量sb引用的對象的內(nèi)部狀態(tài)
sb.append("ForGeeks");
System.out.println(sb);
}
}
輸出:
Geeks
GeeksForGeeks
非傳遞屬性也適用于數(shù)組,因為在Java中數(shù)組也是對象。帶有final關(guān)鍵字的數(shù)組也稱為final數(shù)組。
注意 :
- 如上所述,final變量不能重新賦值,這樣做會拋出編譯時錯誤。
// Java program to demonstrate re-assigning
// final variable will throw compile-time error
class Gfg
{
static final int CAPACITY = 4;
public static void main(String args[])
{
// re-assigning final variable
// will throw compile-time error
CAPACITY = 5;
}
}
輸出:
Compiler Error: cannot assign a value to final variable CAPACITY
- 當(dāng)在方法/構(gòu)造函數(shù)/塊中創(chuàng)建final變量時,它被稱為局部final變量,并且必須在創(chuàng)建它的位置初始化一次。參見下面的局部final變量程序:
// Java program to demonstrate
// local final variable
// The following program compiles and runs fine
class Gfg
{
public static void main(String args[])
{
// local final variable
final int i;
i = 20;
System.out.println(i);
}
}
輸出:
20
- 注意C ++ const變量和Java final變量之間的區(qū)別。聲明時,必須為C ++中的const變量賦值。對于Java中的final變量,正如我們在上面的示例中所看到的那樣,可以稍后賦值,但只能賦值一次。
- final在foreach循環(huán)中:在foreach語句中使用final聲明存儲循環(huán)元素的變量是合法的。
// Java program to demonstrate final
// with for-each statement
class Gfg
{
public static void main(String[] args)
{
int arr[] = {1, 2, 3};
// final with for-each statement
// legal statement
for (final int i : arr)
System.out.print(i + " ");
}
}
輸出:
1 2 3
說明:由于i變量在循環(huán)的每次迭代時超出范圍,因此實際上每次迭代都重新聲明,允許使用相同的標(biāo)記(即i)來表示多個變量。
final 修飾類
當(dāng)使用final關(guān)鍵字聲明一個類時,它被稱為final類。被聲明為final的類不能被擴(kuò)展(繼承)。final類有兩種用途:
- 一個是徹底防止被繼承,因為final類不能被擴(kuò)展。例如,所有包裝類如Integer,Float等都是final類。我們無法擴(kuò)展它們。
- final類的另一個用途是創(chuàng)建一個類似于String類的不可變類。只有將一個類定義成為final類,才能使其不可變。
final class A
{
// methods and fields
}
// 下面的這個類B想要擴(kuò)展類A是非法的
class B extends A
{
// COMPILE-ERROR! Can't subclass A
}
Java支持把class定義成final,似乎違背了面向?qū)ο缶幊痰幕驹瓌t,但在另一方面,封閉的類也保證了該類的所有方法都是固定不變的,不會有子類的覆蓋方法需要去動態(tài)加載。這給編譯器做優(yōu)化時提供了更多的可能,最好的例子是String,它就是final類,Java編譯器就可以把字符串常量(那些包含在雙引號中的內(nèi)容)直接變成String對象,同時對運算符"+"的操作直接優(yōu)化成新的常量,因為final修飾保證了不會有子類對拼接操作返回不同的值。
對于所有不同的類定義一頂層類(全局或包可見)、嵌套類(內(nèi)部類或靜態(tài)嵌套類)都可以用final來修飾。但是一般來說final多用來修飾在被定義成全局(public)的類上,因為對于非全局類,訪問修飾符已經(jīng)將他們限制了它們的也可見性,想要繼承這些類已經(jīng)很困難,就不用再加一層final限制。
final與匿名內(nèi)部類
匿名類(Anonymous Class)雖然說同樣不能被繼承,但它們并沒有被編譯器限制成final。另外要提到的是,網(wǎng)上有許多地方都說因為使用內(nèi)部類,會有兩個地方必須需要使用 final 修飾符:
- 在內(nèi)部類的方法使用到方法中定義的局部變量,則該局部變量需要添加 final 修飾符
- 在內(nèi)部類的方法形參使用到外部傳過來的變量,則形參需要添加 final 修飾符
原因大多是說當(dāng)我們創(chuàng)建匿名內(nèi)部類的那個方法調(diào)用運行完畢之后,因為局部變量的生命周期和方法的生命周期是一樣的,當(dāng)方法彈棧,這個局部變量就會消亡了,但內(nèi)部類對象可能還存在。 此時就會出現(xiàn)一種情況,就是我們調(diào)用這個內(nèi)部類對象去訪問一個不存在的局部變量,就可能會出現(xiàn)空指針異常。而此時需要使用 final 在類加載的時候進(jìn)入常量池,即使方法彈棧,常量池的常量還在,也可以繼續(xù)使用,JVM 會持續(xù)維護(hù)這個引用在回調(diào)方法中的生命周期。
<span style='color:red;'>但是 JDK 1.8 取消了對匿名內(nèi)部類引用的局部變量 final 修飾的檢查</span>
對此,theonlin專門通過實驗做出了總結(jié):其實局部內(nèi)部類并不是直接調(diào)用方法傳進(jìn)來的參數(shù),而是內(nèi)部類將傳進(jìn)來的參數(shù)通過自己的構(gòu)造器備份到了自己的內(nèi)部,自己內(nèi)部的方法調(diào)用的實際是自己的屬性而不是外部類方法的參數(shù)。外部類中的方法中的變量或參數(shù)只是方法的局部變量,這些變量或參數(shù)的作用域只在這個方法內(nèi)部有效,所以方法中被 final的變量的僅僅作用是表明這個變量將作為內(nèi)部類構(gòu)造器參數(shù),其實final不加也可以,加了可能還會占用內(nèi)存空間,影響 GC**。最后結(jié)論就是,需要使用 final 去持續(xù)維護(hù)這個引用在回調(diào)方法中的生命周期這種說法應(yīng)該是錯誤的,也沒必要。
final 修飾方法
下面這段話摘自《Java編程思想》第四版第143頁:
使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。
當(dāng)使用final關(guān)鍵字聲明方法時,它被稱為final方法。final方法無法被覆蓋(重寫)。比如Object類,它的一些方法就被聲明成為了final。如果你認(rèn)為一個方法的功能已經(jīng)足夠完整了,子類中不需要改變的話,你可以聲明此方法為final。以下代碼片段說明了用final關(guān)鍵字修飾方法:
class A
{
// 父類的ml方法被使用了final關(guān)鍵字修飾
final void m1()
{
System.out.println("This is a final method.");
}
}
class B extends A
{
// 此處會報錯,子類B嘗試重寫父類A的被final修飾的ml方法
@override
void m1()
{
// COMPILE-ERROR! Can't override.
System.out.println("Illegal!");
}
}
而關(guān)于高效,是因為在java早期實現(xiàn)中,如果將一個方法指明為final,就是同意編譯器將針對該方法的調(diào)用都轉(zhuǎn)化為內(nèi)嵌調(diào)用(內(nèi)聯(lián))。大概就是,如果是內(nèi)嵌調(diào)用,虛擬機(jī)不再執(zhí)行正常的方法調(diào)用(參數(shù)壓棧,跳轉(zhuǎn)到方法處執(zhí)行,再調(diào)回,處理棧參數(shù),處理返回值),而是直接將方法展開,以方法體中的實際代碼替代原來的方法調(diào)用。這樣減少了方法調(diào)用的開銷。所以有一些程序員認(rèn)為:除非有足夠的理由使用多態(tài)性,否則應(yīng)該將所有的方法都用 final 修飾。這樣的認(rèn)識未免有些偏激,因為在最近的java設(shè)計中,虛擬機(jī)(特別是hotspot技術(shù))可以自己去根據(jù)具體情況自動優(yōu)化選擇是否進(jìn)行內(nèi)聯(lián),只不過使用了final關(guān)鍵字的話可以顯示地影響編譯器對被修飾的代碼進(jìn)行內(nèi)聯(lián)優(yōu)化。所以請切記,對于Java虛擬機(jī)來說編譯器在編譯期間會自動進(jìn)行內(nèi)聯(lián)優(yōu)化,這是由編譯器決定的,對于開發(fā)人員來說,一定要設(shè)計好時空復(fù)雜度的平衡,不要濫用final。
注1:類的private方法會隱式地被指定為final方法,也就同樣無法被重寫。可以對private方法添加final修飾符,但并沒有添加任何額外意義。
注2:在java中,你永遠(yuǎn)不會看到同時使用final和abstract關(guān)鍵字聲明的類或方法。對于類,final用于防止繼承,而抽象類反而需要依賴于它們的子類來完成實現(xiàn)。在修飾方法時,final用于防止被覆蓋,而抽象方法反而需要在子類中被重寫。
有關(guān)final方法和final類的更多示例和行為**,請參閱使用final繼承。
final 優(yōu)化編碼的藝術(shù)
final關(guān)鍵字在效率上的作用主要可以總結(jié)為以下三點:
- 緩存:final配合static關(guān)鍵字提高了代碼性能,JVM和Java應(yīng)用都會緩存final變量。
- 同步:final變量或?qū)ο笫侵蛔x的,可以安全的在多線程環(huán)境下進(jìn)行共享,而不需要額外的同步開銷。
- 內(nèi)聯(lián):使用final關(guān)鍵字,JVM會顯式地主動對方法、變量及類進(jìn)行內(nèi)聯(lián)優(yōu)化。
更多關(guān)于final關(guān)鍵字對代碼的優(yōu)化總結(jié)以及注意點可以參考IBM的《Is that your final answer?》這篇文章。
-
本文原文地址:https://jiang-hao.com/articles/2019/coding-java-final-keyword.html ?
-
本文由筆者參考多篇博文匯總作成,因數(shù)量眾多不一一列出,主體部分從GeeksforGeeks網(wǎng)站翻譯,實際由Gaurav Miglani撰寫。如果您發(fā)現(xiàn)任何不正確的內(nèi)容,或者您想要分享有關(guān)上述主題的更多信息,請撰寫評論。 ?