詳解Java中的final關鍵字


本文原文地址:https://jiang-hao.com/articles/2019/backend-java-final-keyword.html[1]

final 簡介[2]

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

java中的final關鍵字

上面這張圖可以概括成:

  • final修飾變量時,被修飾的變量必須被初始化(賦值),且后續不能修改其值,實質上是常量;
  • final修飾方法時,被修飾的方法無法被所在類的子類重寫(覆寫);
  • final修飾時,被修飾的類不能被繼承,並且final類中的所有成員方法都會被隱式地指定為final方法,但成員變量則不會變。

final 修飾變量

當使用final關鍵字聲明類成員變量或局部變量后,其值不能被再次修改;也經常和static關鍵字一起,作為類常量使用。很多時候會容易把staticfinal關鍵字混淆,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變量有三種方法:

  1. 可以在聲明它時初始化final變量。這種方法是最常見的。如果在聲明時初始化,則該變量稱為final變量。下面是初始化空final變量的兩種方法。
  2. 可以在instance-initializer塊 或內部構造函數中初始化空的final變量。如果您的類中有多個構造函數,則必須在所有構造函數中初始化它,否則將拋出編譯時錯誤。
  3. 可以在靜態塊內初始化空的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引用變量。例如,finalStringBuffer變量:

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數組

注意 :

  1. 如上所述,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
  1. 當在方法/構造函數/塊中創建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
  1. 注意C ++ const變量和Java final變量之間的區別。聲明時,必須為C ++中的const變量賦值。對於Java中的final變量,正如我們在上面的示例中所看到的那樣,可以稍后賦值,但只能賦值一次。
  2. finalforeach循環中:在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類有兩種用途:

  1. 一個是徹底防止被繼承,因為final類不能被擴展。例如,所有包裝類IntegerFloat等都是final類。我們無法擴展它們。
  2. 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 修飾符:

  1. 在內部類的方法使用到方法中定義的局部變量,則該局部變量需要添加 final 修飾符
  2. 在內部類的方法形參使用到外部傳過來的變量,則形參需要添加 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中,你永遠不會看到同時使用finalabstract關鍵字聲明的類或方法。對於類,final用於防止繼承,而抽象類反而需要依賴於它們的子類來完成實現。在修飾方法時,final用於防止被覆蓋,而抽象方法反而需要在子類中被重寫。

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

final 優化編碼的藝術

final關鍵字在效率上的作用主要可以總結為以下三點:

  • 緩存:final配合static關鍵字提高了代碼性能,JVM和Java應用都會緩存final變量。
  • 同步:final變量或對象是只讀的,可以安全的在多線程環境下進行共享,而不需要額外的同步開銷。
  • 內聯:使用final關鍵字,JVM會顯式地主動對方法、變量及類進行內聯優化。

更多關於final關鍵字對代碼的優化總結以及注意點可以參考IBM的《Is that your final answer?》這篇文章。


  1. 本文原文地址:https://jiang-hao.com/articles/2019/backend-java-final-keyword.html ↩︎

  2. 本文由筆者參考多篇博文匯總作成,因數量眾多不一一列出,主體部分從GeeksforGeeks網站翻譯,實際由Gaurav Miglani撰寫。如果您發現任何不正確的內容,或者您想要分享有關上述主題的更多信息,請撰寫評論。 ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM