[Java物件導向]final

Duancf發表於2024-07-26

final 簡介[2]
final關鍵字可用於多個場景,且在不同場景具有不同的作用。首先,final是一個非訪問修飾符,僅適用於變數,方法或類。下面是使用final的不同場景:

java中的final關鍵字

上面這張圖可以概括成:

當final修飾變數時,被修飾的變數必須被初始化(賦值),且後續不能修改其值,實質上是常量;
當final修飾方法時,被修飾的方法無法被所在類的子類重寫(覆寫);
當final修飾類時,被修飾的類不能被繼承,並且final類中的所有成員方法都會被隱式地指定為final方法,但成員變數則不會變。
final 修飾變數
當使用final關鍵字宣告類成員變數或區域性變數後,其值不能被再次修改;也經常和static關鍵字一起,作為類常量使用。很多時候會容易把static和final關鍵字混淆,static作用於成員變數用來表示只儲存一份副本,而final的作用是用來保證變數不可變。如果final變數是引用,這意味著該變數不能重新繫結到引用另一個物件,但是可以更改該引用變數指向的物件的內部狀態,即可以從final陣列或final集合中新增或刪除元素。最好用全部大寫來表示final變數,使用下劃線來分隔單詞。

例子:

//一個final成員常量
final int THRESHOLD = 5;
//一個空的final成員常量
final int THRESHOLD;
//一個靜態final類常量
static final double PI = 3.141592653589793;
//一個空的靜態final類常量
static final double PI;
初始化final變數:

我們必須初始化一個final變數,否則編譯器將丟擲編譯時錯誤。final變數只能透過初始化器或賦值語句初始化一次。初始化final變數有三種方法:

可以在宣告它時初始化final變數。這種方法是最常見的。如果在宣告時未初始化,則該變數稱為空final變數。下面是初始化空final變數的兩種方法。
可以在instance-initializer塊 或內部建構函式中初始化空的final變數。如果您的類中有多個建構函式,則必須在所有建構函式中初始化它,否則將丟擲編譯時錯誤。
可以在靜態塊內初始化空的final靜態變數。
這裡注意有一個很普遍的誤區。很多人會認為static修飾的final常量必須在宣告時就進行初始化,否則會報錯。但其實則不然,我們可以先使用static final關鍵字宣告一個類常量,然後再在靜態塊內初始化空的final靜態變數。讓我們透過一個例子看上面初始化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 
// 直接賦值的靜態final變數
static final double PI = 3.141592653589793; 
  
// a  blank final static variable 
// 空的靜態final變數,此處並不會報錯,因為在下方的靜態程式碼塊內對其進行了初始化
static final double EULERCONSTANT; 
  
// instance initializer block for initializing CAPACITY 
// 用來賦值空final變數的例項初始化塊
{ 
    CAPACITY = 25; 
} 
  
// static initializer block for initializing EULERCONSTANT
// 用來賦值空final變數的靜態初始化塊
static{ 
    EULERCONSTANT = 2.3; 
} 
  
// constructor for initializing MINIMUM 
// Note that if there are more than one 
// constructor, you must initialize MINIMUM 
// in them also 
// 建構函式內初始化空final變數;注意如果有多個
// 建構函式時,必須在每個中都初始化該final變數
public GFG()  
{ 
    MINIMUM = -1; 
} 

}
何時使用final變數:

普通變數和final變數之間的唯一區別是我們可以將值重新賦值給普通變數;但是對於final變數,一旦賦值,我們就不能改變final變數的值。因此,final變數必須僅用於我們希望在整個程式執行期間保持不變的值。

final引用變數:
當final變數是物件的引用時,則此變數稱為final引用變數。例如,final的StringBuffer變數:

final StringBuffer sb;
final變數無法重新賦值。但是對於final的引用變數,可以更改該引用變數指向的物件的內部狀態。請注意,這不是重新賦值。final的這個屬性稱為非傳遞性。要了解物件內部狀態的含義,請參閱下面的示例:

// 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引用的物件的內部狀態
    sb.append("ForGeeks"); 
      
    System.out.println(sb); 
}     

}
輸出:

Geeks
GeeksForGeeks
非傳遞屬性也適用於陣列,因為在Java中陣列也是物件。帶有final關鍵字的陣列也稱為final陣列。

注意 :

如上所述,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
當在方法/建構函式/塊中建立final變數時,它被稱為區域性final變數,並且必須在建立它的位置初始化一次。參見下面的區域性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變數之間的區別。宣告時,必須為C ++中的const變數賦值。對於Java中的final變數,正如我們在上面的示例中所看到的那樣,可以稍後賦值,但只能賦值一次。
final在foreach迴圈中:在foreach語句中使用final宣告儲存迴圈元素的變數是合法的。
// 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變數在迴圈的每次迭代時超出範圍,因此實際上每次迭代都重新宣告,允許使用相同的標記(即i)來表示多個變數。

final 修飾類
當使用final關鍵字宣告一個類時,它被稱為final類。被宣告為final的類不能被擴充套件(繼承)。final類有兩種用途:

一個是徹底防止被繼承,因為final類不能被擴充套件。例如,所有包裝類如Integer,Float等都是final類。我們無法擴充套件它們。
final類的另一個用途是建立一個類似於String類的不可變類。只有將一個類定義成為final類,才能使其不可變。
final class A
{
// methods and fields
}
// 下面的這個類B想要擴充套件類A是非法的
class B extends A
{
// COMPILE-ERROR! Can't subclass A
}
Java支援把class定義成final,似乎違背了物件導向程式設計的基本原則,但在另一方面,封閉的類也保證了該類的所有方法都是固定不變的,不會有子類的覆蓋方法需要去動態載入。這給編譯器做最佳化時提供了更多的可能,最好的例子是String,它就是final類,Java編譯器就可以把字串常量(那些包含在雙引號中的內容)直接變成String物件,同時對運算子"+"的操作直接最佳化成新的常量,因為final修飾保證了不會有子類對拼接操作返回不同的值。
對於所有不同的類定義一頂層類(全域性或包可見)、巢狀類(內部類或靜態巢狀類)都可以用final來修飾。但是一般來說final多用來修飾在被定義成全域性(public)的類上,因為對於非全域性類,訪問修飾符已經將他們限制了它們的也可見性,想要繼承這些類已經很困難,就不用再加一層final限制。

final與匿名內部類

匿名類(Anonymous Class)雖然說同樣不能被繼承,但它們並沒有被編譯器限制成final。另外要提到的是,網上有許多地方都說因為使用內部類,會有兩個地方必須需要使用 final 修飾符:

在內部類的方法使用到方法中定義的區域性變數,則該區域性變數需要新增 final 修飾符
在內部類的方法形參使用到外部傳過來的變數,則形參需要新增 final 修飾符
原因大多是說當我們建立匿名內部類的那個方法呼叫執行完畢之後,因為區域性變數的生命週期和方法的生命週期是一樣的,當方法彈棧,這個區域性變數就會消亡了,但內部類物件可能還存在。 此時就會出現一種情況,就是我們呼叫這個內部類物件去訪問一個不存在的區域性變數,就可能會出現空指標異常。而此時需要使用 final 在類載入的時候進入常量池,即使方法彈棧,常量池的常量還在,也可以繼續使用,JVM 會持續維護這個引用在回撥方法中的生命週期。

但是 JDK 1.8 取消了對匿名內部類引用的區域性變數 final 修飾的檢查

對此,theonlin專門透過實驗做出了總結:其實區域性內部類並不是直接呼叫方法傳進來的引數,而是內部類將傳進來的引數透過自己的構造器備份到了自己的內部,自己內部的方法呼叫的實際是自己的屬性而不是外部類方法的引數。外部類中的方法中的變數或引數只是方法的區域性變數,這些變數或引數的作用域只在這個方法內部有效,所以方法中被 final的變數的僅僅作用是表明這個變數將作為內部類構造器引數,其實final不加也可以,加了可能還會佔用記憶體空間,影響 GC。最後結論就是,需要使用 final 去持續維護這個引用在回撥方法中的生命週期這種說法應該是錯誤的,也沒必要。

final 修飾方法
下面這段話摘自《Java程式設計思想》第四版第143頁:

使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。

當使用final關鍵字宣告方法時,它被稱為final方法。final方法無法被覆蓋(重寫)。比如Object類,它的一些方法就被宣告成為了final。如果你認為一個方法的功能已經足夠完整了,子類中不需要改變的話,你可以宣告此方法為final。以下程式碼片段說明了用final關鍵字修飾方法:

class A
{
// 父類的ml方法被使用了final關鍵字修飾
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!");
}
}
而關於高效,是因為在java早期實現中,如果將一個方法指明為final,就是同意編譯器將針對該方法的呼叫都轉化為內嵌呼叫(內聯)。大概就是,如果是內嵌呼叫,虛擬機器不再執行正常的方法呼叫(引數壓棧,跳轉到方法處執行,再調回,處理棧引數,處理返回值),而是直接將方法展開,以方法體中的實際程式碼替代原來的方法呼叫。這樣減少了方法呼叫的開銷。所以有一些程式設計師認為:除非有足夠的理由使用多型性,否則應該將所有的方法都用 final 修飾。這樣的認識未免有些偏激,因為在最近的java設計中,虛擬機器(特別是hotspot技術)可以自己去根據具體情況自動最佳化選擇是否進行內聯,只不過使用了final關鍵字的話可以顯示地影響編譯器對被修飾的程式碼進行內聯最佳化。所以請切記,對於Java虛擬機器來說編譯器在編譯期間會自動進行內聯最佳化,這是由編譯器決定的,對於開發人員來說,一定要設計好時空複雜度的平衡,不要濫用final。

注1:類的private方法會隱式地被指定為final方法,也就同樣無法被重寫。可以對private方法新增final修飾符,但並沒有新增任何額外意義。

注2:在java中,你永遠不會看到同時使用final和abstract關鍵字宣告的類或方法。對於類,final用於防止繼承,而抽象類反而需要依賴於它們的子類來完成實現。在修飾方法時,final用於防止被覆蓋,而抽象方法反而需要在子類中被重寫。

有關final方法和final類的更多示例和行為,請參閱使用final繼承。

final 最佳化編碼的藝術
final關鍵字在效率上的作用主要可以總結為以下三點:

快取:final配合static關鍵字提高了程式碼效能,JVM和Java應用都會快取final變數。
同步:final變數或物件是隻讀的,可以安全的在多執行緒環境下進行共享,而不需要額外的同步開銷。
內聯:使用final關鍵字,JVM會顯式地主動對方法、變數及類進行內聯最佳化。

相關文章