final 關(guān)鍵字常常被誤用 - 聲明類(lèi)和方法時(shí)使用過(guò)度,而聲明實(shí)例字段時(shí)卻使用不足。本月,Java 實(shí)踐者 Brian Goetz 探究了一些有關(guān)有效使用 final 的準(zhǔn)則。
如同它的“表親”- C 中的 const 關(guān)鍵字一樣,根據(jù)上下文,final 表示不同的東西。final 關(guān)鍵字可應(yīng)用于類(lèi)、方法或字段。應(yīng)用于類(lèi)時(shí),意味著該類(lèi)不能再生成子類(lèi)。應(yīng)用于方法時(shí),意味著該方法不能被子類(lèi)覆蓋。應(yīng)用于字段時(shí),意味著該字段的值在每個(gè)構(gòu)造器內(nèi)必須只能賦值一次而且此后該值永遠(yuǎn)不變。

  大多數(shù) Java 文本都適當(dāng)?shù)孛枋隽耸褂?final 關(guān)鍵字的用法和后果,但是很少以準(zhǔn)則的方式提供有關(guān)何時(shí)使用 final 及使用頻率的內(nèi)容。根據(jù)我的經(jīng)驗(yàn),final 非常過(guò)度地用于類(lèi)和方法(通常是因?yàn)殚_(kāi)發(fā)人員錯(cuò)誤地相信這會(huì)提高性能),而在其用武之地 - 聲明類(lèi)實(shí)例變量 - 卻使用不足。

  為什么這個(gè)類(lèi)是 final?

  對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),將類(lèi)聲明為 final,卻不給出為何作出這一決定的說(shuō)明,這樣的做法很普遍,在開(kāi)放源碼項(xiàng)目中尤其如此。一段時(shí)間之后,特別是如果原來(lái)的開(kāi)發(fā)人員不再參與代碼的維護(hù),其它開(kāi)發(fā)人員將總是發(fā)問(wèn)“為何類(lèi) X 被聲明成 final?”。通常沒(méi)人知道,當(dāng)有人確實(shí)知道或喜歡猜測(cè)時(shí),答案幾乎總是“因?yàn)檫@能使它運(yùn)行得更快”。普遍的理解是:將類(lèi)或方法聲明成 final 會(huì)使編譯器更容易地內(nèi)聯(lián)方法調(diào)用,但是這種理解是不正確的(或者至少說(shuō)是大大地言過(guò)其實(shí)了)。

  final 類(lèi)和方法在編程時(shí)可能是非常大的麻煩 - 它們限制您選擇重用已有的代碼和擴(kuò)展已有類(lèi)的功能。有時(shí)有很好的理由將類(lèi)聲明成 final(如強(qiáng)制不變性),此時(shí)使用 final 的益處將大于其不便之處。性能提高幾乎總是成為破壞良好的面向?qū)ο笤O(shè)計(jì)原則的壞理由,而當(dāng)性能提高很小或者根本沒(méi)有提高時(shí),則它真正是個(gè)很差的權(quán)衡方法。

  過(guò)早優(yōu)化

  出于性能的考慮,在項(xiàng)目的早期階段將方法或類(lèi)聲明成 final 是個(gè)壞主意,這有多個(gè)原因。首先,早期階段設(shè)計(jì)不是考慮循環(huán)計(jì)算性能優(yōu)化的時(shí)候,尤其當(dāng)此類(lèi)決定可能約束您使用 final 進(jìn)行設(shè)計(jì)。其次,通過(guò)將方法或類(lèi)聲明成 final 而獲得的性能優(yōu)勢(shì)通常為零。而且,將復(fù)雜的有狀態(tài)的類(lèi)聲明成 final 不利于面向?qū)ο蟮脑O(shè)計(jì),并導(dǎo)致體積龐大且面面俱到的類(lèi),因?yàn)樗鼈儾荒茌p松地重構(gòu)成更小更緊湊的類(lèi)。

  和許多有關(guān) Java 性能的神話一樣,將類(lèi)或方法聲明成 final 會(huì)帶來(lái)更佳的性能,這一錯(cuò)誤觀念被廣泛接受但極少進(jìn)行檢驗(yàn)。其論點(diǎn)是:將方法或類(lèi)聲明成 final 意味著編譯器可以更加積極地內(nèi)聯(lián)方法調(diào)用,因?yàn)樗涝谶\(yùn)行時(shí)這正是要調(diào)用的方法的版本。但這顯然是不正確的。僅僅因?yàn)轭?lèi) X 編譯成 final 類(lèi) Y,并不意味著同樣版本的類(lèi) Y 將在運(yùn)行時(shí)被裝入。因此編譯器不能安全地內(nèi)聯(lián)這樣的跨類(lèi)方法調(diào)用,不管是不是 final。只有當(dāng)方法是 private 時(shí),編譯器才能自由地內(nèi)聯(lián)它,在這種情況下,final 關(guān)鍵字是多余的。

  另一方面,運(yùn)行時(shí)環(huán)境和 JIT 編譯器擁有更多有關(guān)真正裝入什么類(lèi)的信息,可以比編譯者作出好得多的優(yōu)化決定。如果運(yùn)行時(shí)環(huán)境知道沒(méi)有裝入繼承 Y 的類(lèi),那么它可以安全地內(nèi)聯(lián)對(duì) Y 方法的調(diào)用,不管 Y 是不是 final(只要它能在隨后裝入 Y 子類(lèi)時(shí)使這種 JIT 編譯的代碼無(wú)效)。因此事實(shí)是,盡管 final 對(duì)于不執(zhí)行任何全局相關(guān)性分析的“啞”運(yùn)行時(shí)優(yōu)化器可能是有用的,但它的使用實(shí)際上不支持太多的編譯時(shí)優(yōu)化,而且智能的 JIT 執(zhí)行運(yùn)行時(shí)優(yōu)化時(shí)不需要它。

  似曾相識(shí) - 重新回憶 register 關(guān)鍵字

  final 用于優(yōu)化決定時(shí)和 C 中不贊成使用的 register 關(guān)鍵字非常相似。讓程序員幫助優(yōu)化器這一愿望促成了 register 關(guān)鍵字,但事實(shí)上,發(fā)現(xiàn)這并不是很有用。正如我們?cè)谄渌矫嬖敢庀嘈诺哪菢樱谧鞒龃a優(yōu)化決定方面編譯器通常比人做得出色,在現(xiàn)在的 RISC 處理器上更是如此。事實(shí)上,大多數(shù) C 編譯器完全忽略了 register 關(guān)鍵字。早先的 C 編譯器忽略它是因?yàn)檫@些編譯器根本就不起優(yōu)化作用;現(xiàn)今的編譯器忽略它是因?yàn)榫幾g器不用它就能作更好的優(yōu)化決定。任何一種情況下,register 關(guān)鍵字都沒(méi)有添加什么性能優(yōu)勢(shì),和應(yīng)用于 Java 類(lèi)或方法的 final 關(guān)鍵字很相似。如果您想優(yōu)化您的代碼,請(qǐng)堅(jiān)持使用那些可以大幅度提高性能的優(yōu)化,比如使用有效的算法且不執(zhí)行冗余的計(jì)算 - 將循環(huán)計(jì)算優(yōu)化留給編譯器和 JVM 去做。

  使用 final 保持不變性

  雖然性能并不是將類(lèi)或方法聲明為 final 的好理由,然而有時(shí)仍有充足的理由編寫(xiě) final 類(lèi)。最常見(jiàn)的是 final 保證那些旨在不發(fā)生變化的類(lèi)保持不變。不變類(lèi)對(duì)于簡(jiǎn)化面向?qū)ο蟪绦虻脑O(shè)計(jì)非常有用 - 不變的對(duì)象只需要較少的防御性編碼,并且不要求嚴(yán)格的同步。您不會(huì)在您的代碼中構(gòu)建這一設(shè)想:類(lèi)是不變的,然后讓某些人用使其可變的方式來(lái)繼承它。將不變的類(lèi)聲明成 final 保證了這類(lèi)錯(cuò)誤不會(huì)偷偷溜進(jìn)您的程序中。

  final 用于類(lèi)或方法的另一個(gè)原因是為了防止方法間的鏈接被破壞。例如,假定類(lèi) X 的某個(gè)方法的實(shí)現(xiàn)假設(shè)了方法 M 將以某種方式工作。將 X 或 M 聲明成 final 將防止派生類(lèi)以這種方式重新定義 M,從而導(dǎo)致 X 的工作不正常。盡管不用這些內(nèi)部相關(guān)性來(lái)實(shí)現(xiàn) X 可能會(huì)更好,但這不總是可行的,而且使用 final 可以防止今后這類(lèi)不兼容的更改。

  如果您必須使用 final 類(lèi)或方法,請(qǐng)記錄下為什么這么做

  無(wú)論何種情況,當(dāng)您確實(shí)選擇了將方法或類(lèi)聲明成 final 時(shí),請(qǐng)記錄下為什么這樣做的原因。否則,今后的維護(hù)人員將可能疑惑這樣做是否有好的原因(因?yàn)榻?jīng)常沒(méi)有);而且會(huì)被您的決定所約束,但同時(shí)還不知道您這樣做的動(dòng)機(jī)是為了得到什么好處。在許多情況下,將類(lèi)或方法聲明成 final 的決定一直推遲到開(kāi)發(fā)過(guò)程后期是有意義的,那時(shí)您已經(jīng)擁有關(guān)于類(lèi)是如何交互及可能如何被繼承的更好信息了。您可能發(fā)現(xiàn)您根本不需要將類(lèi)聲明為 final,或者您可以重構(gòu)類(lèi)以便將 final 應(yīng)用于更小更簡(jiǎn)單的類(lèi)。

  final 字段

  final 字段和 final 類(lèi)或方法有很大的不同,以至于我覺(jué)得讓它們共享相同的關(guān)鍵字是不公平的。final 字段是只讀字段,要保證它的值在構(gòu)建時(shí)(或者,對(duì)于 static final 字段,是在類(lèi)初始化時(shí))只設(shè)置一次。正如較早討論的那樣,對(duì)于 final 類(lèi)和方法,您將總是問(wèn)自己是否真的需要使用 final。對(duì)于 final 字段,您將問(wèn)自己相反的問(wèn)題 - 這個(gè)字段真的需要是可變的嗎?您可能會(huì)很驚訝,這個(gè)答案為何常常是“不需要”。

  文檔說(shuō)明價(jià)值

  final 字段有幾個(gè)好處。對(duì)于那些想使用或繼承您的類(lèi)的開(kāi)發(fā)人員來(lái)說(shuō),將字段聲明成 final 有重要的文檔說(shuō)明好處 - 這不僅幫助解釋了該類(lèi)是如何工作的,還獲得了編譯器在加強(qiáng)您的設(shè)計(jì)決定方面的幫助。和 final 方法不同,聲明 final 字段有助于優(yōu)化器作出更好的優(yōu)化決定,因?yàn)槿绻幾g器知道字段的值不會(huì)更改,那么它能安全地在寄存器中高速緩存該值。final 字段還通過(guò)讓編譯器強(qiáng)制該字段為只讀來(lái)提供額外的安全級(jí)別。

  極端情況下,一個(gè)類(lèi),其字段都是 final 原語(yǔ)或?qū)Σ蛔儗?duì)象的 final 引用,那么該類(lèi)本身就變成是不變的 - 事實(shí)上這是一個(gè)非常方便的情況。即使該類(lèi)不是完全不變的,使其某部分狀態(tài)不變可以大大簡(jiǎn)化開(kāi)發(fā) - 您不必為了保證您正在查看 final 字段的當(dāng)前值或者確保沒(méi)有其他人在更改對(duì)象狀態(tài)的這部分而保持同步。

  那么為什么 final 字段使用得如此不足呢?一個(gè)原因是因?yàn)橐_使用它們有點(diǎn)麻煩,對(duì)于其構(gòu)造器能拋出異常的對(duì)象引用來(lái)說(shuō)尤其如此。因?yàn)?final 字段在每個(gè)構(gòu)造器中必須只初始化一次,如果 final 對(duì)象引用的構(gòu)造器可能拋出異常,編譯器可能會(huì)報(bào)錯(cuò),說(shuō)該字段沒(méi)有被初始化。編譯器一般比較智能化,足以發(fā)現(xiàn)在兩個(gè)互斥代碼分支(比如,if...else 塊)的每個(gè)分支中的初始化恰好只進(jìn)行了一次,但是它對(duì) try...catch 塊通常不會(huì)如此“寬容”。例如,大多數(shù) Java 編譯器不會(huì)接受清單 1 中的代碼:

清單 1. final 引用字段的無(wú)效初始化
public class Foo {
  private final Thingie thingie;

  public Foo() {
    try {
      thingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      thingie = Thingie.getDefaultThingie();
    }
  }
}

  但是它們會(huì)接受清單 2 中的代碼,它相當(dāng)于:

清單 2. final 引用字段的有效初始化
public class Foo {
  private final Thingie thingie;

  public Foo() {
    Thingie tempThingie;
    try {
      tempThingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      tempThingie = Thingie.getDefaultThingie();
    }
    thingie = tempThingie;
  }
}

  final 字段的局限性

  final 字段仍然有一些嚴(yán)重的限制。盡管數(shù)組引用能被聲明成 final,但是該數(shù)組的元素卻不能。這意味著暴露 public final 數(shù)組字段的或者通過(guò)它們的方法將引用返回給這些字段的類(lèi)(例如,清單 3 中所示的 DangerousStates 類(lèi))都不是不可改變的。同樣,盡管對(duì)象引用可以被聲明成 final 字段,而它所引用的對(duì)象仍可能是可變的。如果您想要使用 final 字段創(chuàng)建不變的對(duì)象,您必須防止對(duì)數(shù)組或可變對(duì)象的引用“逃離”您的類(lèi)。要不用重復(fù)克隆該數(shù)組做到這一點(diǎn),一個(gè)簡(jiǎn)單的方法是將數(shù)組轉(zhuǎn)變成 List,例如清單 3 中所示的 SafeStates 類(lèi):

清單 3. 暴露數(shù)組引用使類(lèi)成為可變的
// Not immutable -- the states array could be modified by a malicious
caller
public class DangerousStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };

  public String[] getStates() {
    return states;
  }
}


// Immutable -- returns an unmodifiable List instead
public class SafeStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };
  private final List statesAsList
    = new AbstractList() {
        public Object get(int n) {
          return states[n];
        }

        public int size() {
          return states.length;
        }
      };
       
  public List getStates() {
    return statesAsList;
  }
}

  為什么不繼承 final 以應(yīng)用于數(shù)組和引用的對(duì)象,類(lèi)似于 C 和 C++ 中 const 的使用那樣呢?C++ 中 const 的語(yǔ)義和使用相當(dāng)混淆,根據(jù)它在表達(dá)式中所出現(xiàn)的位置表示不同的東西。Java 架構(gòu)設(shè)計(jì)師設(shè)法把我們從這種混淆中“解救”出來(lái),但遺憾的是他們?cè)谶@個(gè)過(guò)程中產(chǎn)生出了一些新的混淆。

  結(jié)束語(yǔ)

  要對(duì)類(lèi)、方法和字段有效使用 final,有一些基本的準(zhǔn)則可以遵循。特別要注意的是,不要嘗試將 final 用作性能管理工具;要提高您的程序的性能,有更好且約束更少的方法。在反映您程序的基本語(yǔ)義處使用 final:用來(lái)指示這些類(lèi)將是不可改變的或那些字段將是只讀的。如果您選擇創(chuàng)建 final 類(lèi)或方法,請(qǐng)確保您清楚地記錄您為何這么做 - 您的同事會(huì)感激您的。



cxzforever