詳解Java中的final關(guān)鍵字

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

final 簡介[2]

final關(guān)鍵字可用于多個(gè)場景,且在不同場景具有不同的作用橄杨。首先片习,final是一個(gè)非訪問修飾符適用于變量枪眉,方法或類捺檬。下面是使用final的不同場景:

[圖片上傳失敗...(image-bee40-1555334312011)]

上面這張圖可以概括成:

  • 當(dāng)final修飾變量時(shí),被修飾的變量必須被初始化(賦值)贸铜,且后續(xù)不能修改其值堡纬,實(shí)質(zhì)上是常量;
  • 當(dāng)final修飾方法時(shí)蒿秦,被修飾的方法無法被所在類的子類重寫(覆寫)烤镐;
  • 當(dāng)final修飾時(shí),被修飾的類不能被繼承棍鳖,并且final類中的所有成員方法都會(huì)被隱式地指定為final方法炮叶,但成員變量則不會(huì)變。

final 修飾變量

當(dāng)使用final關(guān)鍵字聲明類成員變量或局部變量后渡处,其值不能被再次修改镜悉;也經(jīng)常和static關(guān)鍵字一起,作為類常量使用医瘫。很多時(shí)候會(huì)容易把staticfinal關(guān)鍵字混淆侣肄,<u>static作用于成員變量用來表示只保存一份副本,而final的作用是用來保證變量不可變</u>醇份。如果final變量是引用茫孔,這意味著該變量不能重新綁定到引用另一個(gè)對象叮喳,但是可以更改該引用變量指向的對象的內(nèi)部狀態(tài),即可以從final數(shù)組final集合中添加或刪除元素缰贝。最好用全部大寫來表示final變量馍悟,使用下劃線來分隔單詞。

例子

//一個(gè)final成員常量
final int THRESHOLD = 5;
//一個(gè)空的final成員常量
final int THRESHOLD;
//一個(gè)靜態(tài)final類常量
static final double PI = 3.141592653589793;
//一個(gè)空的靜態(tài)final類常量
static final double PI;

初始化final變量

我們必須初始化一個(gè)final變量剩晴,否則編譯器將拋出編譯時(shí)錯(cuò)誤锣咒。final變量只能通過初始化器或賦值語句初始化一次。初始化final變量有三種方法:

  1. 可以在聲明它時(shí)初始化final變量赞弥。這種方法是最常見的毅整。如果在聲明時(shí)初始化,則該變量稱為final變量绽左。下面是初始化空final變量的兩種方法悼嫉。
  2. 可以在instance-initializer塊 或內(nèi)部構(gòu)造函數(shù)中初始化空的final變量。如果您的類中有多個(gè)構(gòu)造函數(shù)拼窥,則必須在所有構(gòu)造函數(shù)中初始化它戏蔑,否則將拋出編譯時(shí)錯(cuò)誤。
  3. 可以在靜態(tài)塊內(nèi)初始化空的final靜態(tài)變量鲁纠。

這里注意有一個(gè)很普遍的誤區(qū)总棵。<u>很多人會(huì)認(rèn)為static修飾的final常量必須在聲明時(shí)就進(jìn)行初始化,否則會(huì)報(bào)錯(cuò)改含。但其實(shí)則不然情龄,我們可以先使用static final關(guān)鍵字聲明一個(gè)類常量,然后再在靜態(tài)塊內(nèi)初始化空的final靜態(tài)變量捍壤。</u>讓我們通過一個(gè)例子看上面初始化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變量,此處并不會(huì)報(bào)錯(cuò)鹃觉,因?yàn)樵谙路降撵o態(tài)代碼塊內(nèi)對其進(jìn)行了初始化
    static final double EULERCONSTANT; 
      
    // instance initializer block for initializing CAPACITY 
    // 用來賦值空final變量的實(shí)例初始化塊
    { 
        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è)
    // 構(gòu)造函數(shù)時(shí),必須在每個(gè)中都初始化該final變量
    public GFG()  
    { 
        MINIMUM = -1; 
    } 
          
} 

何時(shí)使用final變量:**

普通變量和final變量之間的唯一區(qū)別是我們可以將值重新賦值給普通變量帜慢;但是對于final變量笼裳,一旦賦值唯卖,我們就不能改變final變量的值粱玲。因此,final變量必須僅用于我們希望在整個(gè)程序執(zhí)行期間保持不變的值拜轨。

final引用變量:
當(dāng)final變量是對象的引用時(shí)抽减,則此變量稱為final引用變量。例如橄碾,finalStringBuffer變量:

final StringBuffer sb;

final變量無法重新賦值卵沉。但是對于final的引用變量颠锉,可以更改該引用變量指向的對象的內(nèi)部狀態(tài)。請注意史汗,這不是重新賦值琼掠。final的這個(gè)屬性稱為非傳遞性。要了解對象內(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ù)組瓷蛙,因?yàn)樵贘ava中數(shù)組也是對象。帶有final關(guān)鍵字的數(shù)組也稱為final數(shù)組戈毒。

注意 :

  1. 如上所述艰猬,final變量不能重新賦值,這樣做會(huì)拋出編譯時(shí)錯(cuò)誤埋市。
   // 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. 當(dāng)在方法/構(gòu)造函數(shù)/塊中創(chuàng)建final變量時(shí)冠桃,它被稱為局部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
  1. 注意C ++ const變量和Java final變量之間的區(qū)別食听。聲明時(shí),必須為C ++中的const變量賦值培己。對于Java中的final變量碳蛋,正如我們在上面的示例中所看到的那樣,可以稍后賦值省咨,但只能賦值一次肃弟。
  2. finalforeach循環(huán)中:在foreach語句中使用final聲明存儲(chǔ)循環(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)的每次迭代時(shí)超出范圍零蓉,因此實(shí)際上每次迭代都重新聲明笤受,允許使用相同的標(biāo)記(即i)來表示多個(gè)變量。

final 修飾類

當(dāng)使用final關(guān)鍵字聲明一個(gè)類時(shí)敌蜂,它被稱為final類箩兽。被聲明為final的類不能被擴(kuò)展(繼承)。final類有兩種用途:

  1. 一個(gè)是徹底防止被繼承章喉,因?yàn)?em>final類不能被擴(kuò)展汗贫。例如,所有包裝類Integer秸脱,Float等都是final類落包。我們無法擴(kuò)展它們。
  2. final類的另一個(gè)用途是創(chuàng)建一個(gè)類似于String類的不可變類摊唇。只有將一個(gè)類定義成為final類咐蝇,才能使其不可變。
  final class A
  {
       // methods and fields
  }
  // 下面的這個(gè)類B想要擴(kuò)展類A是非法的
  class B extends A 
  { 
      // COMPILE-ERROR! Can't subclass A
  }

Java支持把class定義成final巷查,似乎違背了面向?qū)ο缶幊痰幕驹瓌t有序,但在另一方面抹腿,封閉的類也保證了該類的所有方法都是固定不變的,不會(huì)有子類的覆蓋方法需要去動(dòng)態(tài)加載旭寿。這給編譯器做優(yōu)化時(shí)提供了更多的可能警绩,最好的例子是String,它就是final類盅称,Java編譯器就可以把字符串常量(那些包含在雙引號(hào)中的內(nèi)容)直接變成String對象房蝉,同時(shí)對運(yùn)算符"+"的操作直接優(yōu)化成新的常量,因?yàn)閒inal修飾保證了不會(huì)有子類對拼接操作返回不同的值微渠。
對于所有不同的類定義一頂層類(全局或包可見)搭幻、嵌套類(內(nèi)部類或靜態(tài)嵌套類)都可以用final來修飾。但是一般來說final多用來修飾在被定義成全局(public)的類上逞盆,因?yàn)閷τ诜侨诸愄刺#L問修飾符已經(jīng)將他們限制了它們的也可見性,想要繼承這些類已經(jīng)很困難云芦,就不用再加一層final限制俯逾。

final與匿名內(nèi)部類

匿名類(Anonymous Class)雖然說同樣不能被繼承,但它們并沒有被編譯器限制成final舅逸。另外要提到的是桌肴,網(wǎng)上有許多地方都說因?yàn)槭褂脙?nèi)部類,會(huì)有兩個(gè)地方必須需要使用 final 修飾符:

  1. 在內(nèi)部類的方法使用到方法中定義的局部變量琉历,則該局部變量需要添加 final 修飾符
  2. 在內(nèi)部類的方法形參使用到外部傳過來的變量坠七,則形參需要添加 final 修飾符

原因大多是說當(dāng)我們創(chuàng)建匿名內(nèi)部類的那個(gè)方法調(diào)用運(yùn)行完畢之后,因?yàn)榫植孔兞康纳芷诤头椒ǖ纳芷谑且粯拥钠毂剩?dāng)方法彈棧彪置,這個(gè)局部變量就會(huì)消亡了,但內(nèi)部類對象可能還存在蝇恶。 此時(shí)就會(huì)出現(xiàn)一種情況拳魁,就是我們調(diào)用這個(gè)內(nèi)部類對象去訪問一個(gè)不存在的局部變量,就可能會(huì)出現(xiàn)空指針異常撮弧。而此時(shí)需要使用 final 在類加載的時(shí)候進(jìn)入常量池潘懊,即使方法彈棧,常量池的常量還在贿衍,也可以繼續(xù)使用授舟,JVM 會(huì)持續(xù)維護(hù)這個(gè)引用在回調(diào)方法中的生命周期。

<span style='color:red;'>但是 JDK 1.8 取消了對匿名內(nèi)部類引用的局部變量 final 修飾的檢查</span>

對此舌厨,theonlin專門通過實(shí)驗(yàn)做出了總結(jié):其實(shí)局部內(nèi)部類并不是直接調(diào)用方法傳進(jìn)來的參數(shù)岂却,而是內(nèi)部類將傳進(jìn)來的參數(shù)通過自己的構(gòu)造器備份到了自己的內(nèi)部忿薇,自己內(nèi)部的方法調(diào)用的實(shí)際是自己的屬性而不是外部類方法的參數(shù)裙椭。外部類中的方法中的變量或參數(shù)只是方法的局部變量躏哩,這些變量或參數(shù)的作用域只在這個(gè)方法內(nèi)部有效,所以方法中被 final的變量的僅僅作用是表明這個(gè)變量將作為內(nèi)部類構(gòu)造器參數(shù)揉燃,其實(shí)final不加也可以扫尺,加了可能還會(huì)占用內(nèi)存空間,影響 GC**炊汤。最后結(jié)論就是正驻,需要使用 final 去持續(xù)維護(hù)這個(gè)引用在回調(diào)方法中的生命周期這種說法應(yīng)該是錯(cuò)誤的,也沒必要抢腐。

final 修飾方法

下面這段話摘自《Java編程思想》第四版第143頁:

使用final方法的原因有兩個(gè)姑曙。第一個(gè)原因是把方法鎖定,以防任何繼承類修改它的含義迈倍;第二個(gè)原因是效率伤靠。

當(dāng)使用final關(guān)鍵字聲明方法時(shí),它被稱為final方法啼染。final方法無法被覆蓋(重寫)宴合。比如Object類,它的一些方法就被聲明成為了final迹鹅。如果你認(rèn)為一個(gè)方法的功能已經(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 
{
    // 此處會(huì)報(bào)錯(cuò)阀蒂,子類B嘗試重寫父類A的被final修飾的ml方法
    @override
    void m1()
    { 
        // COMPILE-ERROR! Can't override.
        System.out.println("Illegal!");
    }
}

而關(guān)于高效,是因?yàn)樵趈ava早期實(shí)現(xiàn)中弟蚀,如果將一個(gè)方法指明為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)回滞乙,處理?xiàng)?shù),處理返回值)鉴嗤,而是直接將方法展開斩启,以方法體中的實(shí)際代碼替代原來的方法調(diào)用。這樣減少了方法調(diào)用的開銷醉锅。所以有一些程序員認(rèn)為:除非有足夠的理由使用多態(tài)性兔簇,否則應(yīng)該將所有的方法都用 final 修飾。這樣的認(rèn)識(shí)未免有些偏激,因?yàn)樵谧罱膉ava設(shè)計(jì)中垄琐,虛擬機(jī)(特別是hotspot技術(shù))可以自己去根據(jù)具體情況自動(dòng)優(yōu)化選擇是否進(jìn)行內(nèi)聯(lián)边酒,只不過使用了final關(guān)鍵字的話可以顯示地影響編譯器對被修飾的代碼進(jìn)行內(nèi)聯(lián)優(yōu)化。所以請切記狸窘,對于Java虛擬機(jī)來說編譯器在編譯期間會(huì)自動(dòng)進(jìn)行內(nèi)聯(lián)優(yōu)化墩朦,這是由編譯器決定的,對于開發(fā)人員來說翻擒,一定要設(shè)計(jì)好時(shí)空復(fù)雜度的平衡氓涣,不要濫用final。

注1:類的private方法會(huì)隱式地被指定為final方法陋气,也就同樣無法被重寫劳吠。可以對private方法添加final修飾符巩趁,但并沒有添加任何額外意義赴背。

注2:在java中,你永遠(yuǎn)不會(huì)看到同時(shí)使用finalabstract關(guān)鍵字聲明的類或方法晶渠。對于類凰荚,final用于防止繼承,而抽象類反而需要依賴于它們的子類來完成實(shí)現(xiàn)褒脯。在修飾方法時(shí)便瑟,final用于防止被覆蓋,而抽象方法反而需要在子類中被重寫番川。

有關(guān)final方法和final類的更多示例和行為**到涂,請參閱使用final繼承

final 優(yōu)化編碼的藝術(shù)

final關(guān)鍵字在效率上的作用主要可以總結(jié)為以下三點(diǎn):

  • 緩存:final配合static關(guān)鍵字提高了代碼性能颁督,JVM和Java應(yīng)用都會(huì)緩存final變量践啄。
  • 同步:final變量或?qū)ο笫侵蛔x的,可以安全的在多線程環(huán)境下進(jìn)行共享沉御,而不需要額外的同步開銷屿讽。
  • 內(nèi)聯(lián):使用final關(guān)鍵字,JVM會(huì)顯式地主動(dòng)對方法吠裆、變量及類進(jìn)行內(nèi)聯(lián)優(yōu)化伐谈。

更多關(guān)于final關(guān)鍵字對代碼的優(yōu)化總結(jié)以及注意點(diǎn)可以參考IBM的《Is that your final answer?》這篇文章。


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

  2. 本文由筆者參考多篇博文匯總作成试疙,因數(shù)量眾多不一一列出诵棵,主體部分從GeeksforGeeks網(wǎng)站翻譯,實(shí)際由Gaurav Miglani撰寫祝旷。如果您發(fā)現(xiàn)任何不正確的內(nèi)容履澳,或者您想要分享有關(guān)上述主題的更多信息嘶窄,請撰寫評論。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末距贷,一起剝皮案震驚了整個(gè)濱河市柄冲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌储耐,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滨溉,死亡現(xiàn)場離奇詭異什湘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)晦攒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門闽撤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脯颜,你說我怎么就攤上這事哟旗。” “怎么了栋操?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵闸餐,是天一觀的道長。 經(jīng)常有香客問我矾芙,道長舍沙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任剔宪,我火速辦了婚禮拂铡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘葱绒。我一直安慰自己感帅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布地淀。 她就那樣靜靜地躺著失球,像睡著了一般。 火紅的嫁衣襯著肌膚如雪帮毁。 梳的紋絲不亂的頭發(fā)上她倘,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音作箍,去河邊找鬼硬梁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胞得,可吹牛的內(nèi)容都是我干的荧止。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼跃巡!你這毒婦竟也來了危号?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤素邪,失蹤者是張志新(化名)和其女友劉穎外莲,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兔朦,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡偷线,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沽甥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片声邦。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摆舟,靈堂內(nèi)的尸體忽然破棺而出亥曹,到底是詐尸還是另有隱情,我是刑警寧澤恨诱,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布媳瞪,位于F島的核電站,受9級特大地震影響照宝,放射性物質(zhì)發(fā)生泄漏材失。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一硫豆、第九天 我趴在偏房一處隱蔽的房頂上張望龙巨。 院中可真熱鬧,春花似錦熊响、人聲如沸旨别。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秸弛。三九已至,卻和暖如春洪碳,著一層夾襖步出監(jiān)牢的瞬間递览,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工瞳腌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绞铃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓嫂侍,卻偏偏與公主長得像儿捧,于是被迫代替她去往敵國和親荚坞。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348