程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 有效使用final關鍵字的准則

有效使用final關鍵字的准則

編輯:關於JAVA

  final 關鍵字常常被誤用 - 聲明類和方法時使用過度,而聲明實例字段時卻使用不足。本月,Java 實踐者 Brian Goetz 探究了一些有關有效使用 final 的准則。

  如同它的“表親”- C 中的 const 關鍵字一樣,根據上下文,final 表示不同的東西。final 關鍵字可應用於類、方法或字段。應用於類時,意味著該類不能再生成子類。應用於方法時,意味著該方法不能被子類覆蓋。應用於字段時,意味著該字段的值在每個構造器內必須只能賦值一次而且此後該值永遠不變。

  大多數 Java 文本都適當地描述了使用 final 關鍵字的用法和後果,但是很少以准則的方式提供有關何時使用 final 及使用頻率的內容。根據我的經驗,final 非常過度地用於類和方法(通常是因為開發人員錯誤地相信這會提高性能),而在其用武之地 - 聲明類實例變量 - 卻使用不足。

  為什麼這個類是 final?

  對於開發人員來說,將類聲明為 final,卻不給出為何作出這一決定的說明,這樣的做法很普遍,在開放源碼項目中尤其如此。一段時間之後,特別是如果原來的開發人員不再參與代碼的維護,其它開發人員將總是發問“為何類 X 被聲明成 final?”。通常沒人知道,當有人確實知道或喜歡猜測時,答案幾乎總是“因為這能使它運行得更快”。普遍的理解是:將類或方法聲明成 final 會使編譯器更容易地內聯方法調用,但是這種理解是不正確的(或者至少說是大大地言過其實了)。

  final 類和方法在編程時可能是非常大的麻煩 - 它們限制您選擇重用已有的代碼和擴展已有類的功能。有時有很好的理由將類聲明成 final(如強制不變性),此時使用 final 的益處將大於其不便之處。性能提高幾乎總是成為破壞良好的面向對象設計原則的壞理由,而當性能提高很小或者根本沒有提高時,則它真正是個很差的權衡方法。

  過早優化

  出於性能的考慮,在項目的早期階段將方法或類聲明成 final 是個壞主意,這有多個原因。首先,早期階段設計不是考慮循環計算性能優化的時候,尤其當此類決定可能約束您使用 final 進行設計。其次,通過將方法或類聲明成 final 而獲得的性能優勢通常為零。而且,將復雜的有狀態的類聲明成 final 不利於面向對象的設計,並導致體積龐大且面面俱到的類,因為它們不能輕松地重構成更小更緊湊的類。

  和許多有關 Java 性能的神話一樣,將類或方法聲明成 final 會帶來更佳的性能,這一錯誤觀念被廣泛接受但極少進行檢驗。其論點是:將方法或類聲明成 final 意味著編譯器可以更加積極地內聯方法調用,因為它知道在運行時這正是要調用的方法的版本。但這顯然是不正確的。僅僅因為類 X 編譯成 final 類 Y,並不意味著同樣版本的類 Y 將在運行時被裝入。因此編譯器不能安全地內聯這樣的跨類方法調用,不管是不是 final。只有當方法是 private 時,編譯器才能自由地內聯它,在這種情況下,final 關鍵字是多余的。

  另一方面,運行時環境和 JIT 編譯器擁有更多有關真正裝入什麼類的信息,可以比編譯者作出好得多的優化決定。如果運行時環境知道沒有裝入繼承 Y 的類,那麼它可以安全地內聯對 Y 方法的調用,不管 Y 是不是 final(只要它能在隨後裝入 Y 子類時使這種 JIT 編譯的代碼無效)。因此事實是,盡管 final 對於不執行任何全局相關性分析的“啞”運行時優化器可能是有用的,但它的使用實際上不支持太多的編譯時優化,而且智能的 JIT 執行運行時優化時不需要它。

  似曾相識 - 重新回憶 register 關鍵字

  final 用於優化決定時和 C 中不贊成使用的 register 關鍵字非常相似。讓程序員幫助優化器這一願望促成了 register 關鍵字,但事實上,發現這並不是很有用。正如我們在其它方面願意相信的那樣,在作出代碼優化決定方面編譯器通常比人做得出色,在現在的 RISC 處理器上更是如此。事實上,大多數 C 編譯器完全忽略了 register 關鍵字。早先的 C 編譯器忽略它是因為這些編譯器根本就不起優化作用;現今的編譯器忽略它是因為編譯器不用它就能作更好的優化決定。任何一種情況下,register 關鍵字都沒有添加什麼性能優勢,和應用於 Java 類或方法的 final 關鍵字很相似。如果您想優化您的代碼,請堅持使用那些可以大幅度提高性能的優化,比如使用有效的算法且不執行冗余的計算 - 將循環計算優化留給編譯器和 JVM 去做。

  使用 final 保持不變性

  雖然性能並不是將類或方法聲明為 final 的好理由,然而有時仍有充足的理由編寫 final 類。最常見的是 final 保證那些旨在不發生變化的類保持不變。不變類對於簡化面向對象程序的設計非常有用 - 不變的對象只需要較少的防御性編碼,並且不要求嚴格的同步。您不會在您的代碼中構建這一設想:類是不變的,然後讓某些人用使其可變的方式來繼承它。將不變的類聲明成 final 保證了這類錯誤不會偷偷溜進您的程序中。

  final 用於類或方法的另一個原因是為了防止方法間的鏈接被破壞。例如,假定類 X 的某個方法的實現假設了方法 M 將以某種方式工作。將 X 或 M 聲明成 final 將防止派生類以這種方式重新定義 M,從而導致 X 的工作不正常。盡管不用這些內部相關性來實現 X 可能會更好,但這不總是可行的,而且使用 final 可以防止今後這類不兼容的更改。

  如果您必須使用 final 類或方法,請記錄下為什麼這麼做

  無論何種情況,當您確實選擇了將方法或類聲明成 final 時,請記錄下為什麼這樣做的原因。否則,今後的維護人員將可能疑惑這樣做是否有好的原因(因為經常沒有);而且會被您的決定所約束,但同時還不知道您這樣做的動機是為了得到什麼好處。在許多情況下,將類或方法聲明成 final 的決定一直推遲到開發過程後期是有意義的,那時您已經擁有關於類是如何交互及可能如何被繼承的更好信息了。您可能發現您根本不需要將類聲明為 final,或者您可以重構類以便將 final 應用於更小更簡單的類。

  final 字段

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

  文檔說明價值

  final 字段有幾個好處。對於那些想使用或繼承您的類的開發人員來說,將字段聲明成 final 有重要的文檔說明好處 - 這不僅幫助解釋了該類是如何工作的,還獲得了編譯器在加強您的設計決定方面的幫助。和 final 方法不同,聲明 final 字段有助於優化器作出更好的優化決定,因為如果編譯器知道字段的值不會更改,那麼它能安全地在寄存器中高速緩存該值。final 字段還通過讓編譯器強制該字段為只讀來提供額外的安全級別。

  極端情況下,一個類,其字段都是 final 原語或對不變對象的 final 引用,那麼該類本身就變成是不變的 - 事實上這是一個非常方便的情況。即使該類不是完全不變的,使其某部分狀態不變可以大大簡化開發 - 您不必為了保證您正在查看 final 字段的當前值或者確保沒有其他人在更改對象狀態的這部分而保持同步。

  那麼為什麼 final 字段使用得如此不足呢?一個原因是因為要正確使用它們有點麻煩,對於其構造器能拋出異常的對象引用來說尤其如此。因為 final 字段在每個構造器中必須只初始化一次,如果 final 對象引用的構造器可能拋出異常,編譯器可能會報錯,說該字段沒有被初始化。編譯器一般比較智能化,足以發現在兩個互斥代碼分支(比如,if...else 塊)的每個分支中的初始化恰好只進行了一次,但是它對 try...catch 塊通常不會如此“寬容”。例如,大多數 Java 編譯器不會接受清單 1 中的代碼:

清單 1. final 引用字段的無效初始化
以下為引用的內容:
public class Foo {
  private final Thingie thingIE;

  public Foo() {
    try {
      thingie = new ThingIE();
    }
    catch (ThingIEConstructionException e) {
      thingie = Thingie.getDefaultThingIE();
    }
  }
}

  但是它們會接受清單 2 中的代碼,它相當於:

清單 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 字段仍然有一些嚴重的限制。盡管數組引用能被聲明成 final,但是該數組的元素卻不能。這意味著暴露 public final 數組字段的或者通過它們的方法將引用返回給這些字段的類(例如,清單 3 中所示的 DangerousStates 類)都不是不可改變的。同樣,盡管對象引用可以被聲明成 final 字段,而它所引用的對象仍可能是可變的。如果您想要使用 final 字段創建不變的對象,您必須防止對數組或可變對象的引用“逃離”您的類。要不用重復克隆該數組做到這一點,一個簡單的方法是將數組轉變成 List,例如清單 3 中所示的 SafeStates 類:

清單 3. 暴露數組引用使類成為可變的
以下為引用的內容:
// 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 以應用於數組和引用的對象,類似於 C 和 C++ 中 const 的使用那樣呢?C++ 中 const 的語義和使用相當混淆,根據它在表達式中所出現的位置表示不同的東西。Java 架構設計師設法把我們從這種混淆中“解救”出來,但遺憾的是他們在這個過程中產生出了一些新的混淆。

  結束語

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved