Java 理論和實踐:那是您的最終答案嗎?

來源:互聯網
上載者:User
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 類或方法,請確保您清楚地記錄您為何這麼做 - 您的同事會感激您的。



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.