Effective Java 3rd 條目24 靜態成員類優於非靜態

weixin_33860722發表於2018-02-27

巢狀類(nested class)是一個定義在另外一個類內部的類。巢狀類應該僅僅是為了服務外部類而存在。如果內嵌類在其他某些情形下有用,那麼他應該是一個頂層類。有四種巢狀類:靜態成員類(static member class)非靜態成員類(nonstatic member class)匿名類(anonymous class)本地類(local class)。只有第一種是被認為是內部類。這個條目告訴你宣告時候使用哪種巢狀類以及為什麼。

靜態成員類是巢狀類的最簡單的種類。它最好被認為是一個普通類,它只是恰好宣告在另外一個類的內部,而且可以訪問外部類的所有成員,即使這些成員是宣告為私有的。靜態成員類是它的外部類的靜態成員,而且就像其他靜態成員一樣遵從同樣的訪問規則。如果它是宣告為私有的,那麼他僅僅可以在外部類內部訪問,諸如此類。

靜態內部類的一個通常使用是作為一個公開協助類,僅僅是和它的外部類聯合使用。例如,考慮一個列舉,它描述由計算器支援的操作 (條目34)。Operation列舉應該是Calculator類的公開靜態成員。於是Calculator的客戶端應該引用一些操作,這些操作使用像Calculator.Operation.PLUS和Calculator.Operation.MINUS這樣的名字。

在語句構成上,靜態和非靜態成員類的唯一區別在於,靜態成員類在它們的宣告中有修飾符static。儘管語法上的相似性,但是這兩種巢狀類是非常不同的。非靜態成員類的每個例項與包含類的外部例項相關聯。在非靜態成員類的例項方法裡面,你可以呼叫外部例項(enclosing instance)的方法,或者使用限定的(qualified)this結構體獲得外部例項的引用 [JLS, 15.8.4]。如果巢狀類的例項的存在脫離它的外部類的例項,那麼巢狀類必須是一個靜態成員類:沒有外部例項,建立一個非靜態成員類是不可能的。

當成員類例項建立時,非靜態成員類例項和它的外部例項的聯絡建立,而且在這之後不能改變。通常,這個聯絡是從外部類的例項方法內部,通過呼叫一個非靜態成員類構造子自動建立的。手動使用enclosingInstance.new MemberClass(args)建立這個聯絡,也是可能的,雖然非常少見。就像你所預料的,這個聯絡在非靜態成員類例項中佔用了空間,而且它的構造過程增添了時間。

非靜態成員類的一個通常使用是定義一個Adapter[Gamma95],它允許外部類的例項被看成是某個不相關類的例項。例如,Map介面的實現通常使用非靜態成員類實現他們的集合檢視(collection view),它們是由Map’s keySet、entrySet和values返回。相似地,集合介面,比如Set和List,它們的實現通常使用非靜態成員類來實現它們的迭代器:

// 非靜態成員類的典型使用 
public class MySet<E> extends AbstractSet<E> { 
    ... // 這個類的大部分省略

    @Override public Iterator<E> iterator() { 
        return new MyIterator(); 
    }

    private class MyIterator implements Iterator<E> { 
        ...
    }
}

如果你定義一個成員類,它不需要訪問外部例項,那麼永遠把static修飾符放在它的宣告中,讓它成為一個靜態而不是非靜態成員類。如果你省略這個修飾符,那麼每個例項將有一個它的外部例項的額外隱含引用。就像前面提到的,儲存這個引用耗費時間和空間。更為嚴重的是,它可能導致外部例項留存,原本它是適合垃圾收集(條目7)。最終的記憶體洩漏可能是災難性的。它通常很難監測到,因為這個引用是不可見的。

私有靜態成員類的一個通常使用是代表物件的元件,這個物件由它們外部類表示。例如,考慮Map例項,它把鍵和值聯絡起來。許多Map例項,對於對映中每個鍵值對,有一個內部Entry例項。雖然每個entry和一個對映相聯絡,但是entry的方法(getKey、getValue和setValue)不需要訪問對映。所以,使用非靜態成員類來表示entry是很浪費的:私有靜態成員類是最好的。如果你在entry宣告中不慎忽略了static修飾符,這個對映仍然是工作的,但是每個entry將包含一個多餘的對對映的引用,這就浪費了空間和時間。

如果正在討論的類是一個匯出類的公開或者受保護成員,在一個靜態和非靜態成員類之間正確地選擇是非常重要的。在這種情況下,成員類是一個匯出API元素,而且在後續釋出中不能把它從非靜態改變為靜態成員類,而不會違反向後相容性。

就像你所預料的,匿名類沒有名字。它不是它的外部類的一個成員。不是和其他成員一起宣告,它是在使用時同時宣告和例項化的。程式碼中一個表示式合法的任何地方,匿名類也是允許的。當且僅當它們在非靜態環境中發生,匿名類有外部類。但是即使它們在靜態環境中發生,它們也不能夠有任何靜態成員,除了常數變數(constant variable),它是初始化為常數表示式的final原始或者字串域,[JLS, 4.12.4]。

匿名類的應用有許多限制。除非在它們被宣告時,你不能夠例項化它們。你不能進行instanceof測試,或者做需要你命名這個類的任何其他事情。你不能宣告一個匿名類來實現多個介面,或者同時擴充套件一個類和實現一個介面。使用匿名類的客戶端不能夠呼叫任何成員,除了從它的超類繼承而來的那些成員。因為匿名類發生在表示式之中,它們應該保持簡短(大約十行或者更少),否則可讀性將會受損。

在Java新增lambda(第6章)之前,匿名類是隨手建立小函式物件(function object)處理物件(process object)的優選方式,而且,但是lambda現在是更優的(條目42)。匿名類的另外一個通常使用是靜態工廠方法的實現(參考條目20中intArrayAsList)。

本地類是四種內嵌類中最少使用。一個本地類實際上可以在一個本地變數可以宣告的任何地方宣告,而且遵從相同的作用域規則。本地類和其他型別的巢狀類有許多相同的特質。像成員類,它們有名字,而且可以重複使用。像匿名類,只有當它們在非靜態情形中定義時,它們有外部例項,而且它們不能保護靜態成員。而且像匿名類,為了不傷害可讀性,它們應該保持簡短。

簡要概括,有四種不同巢狀類,而且每種有它的位置。如果一個巢狀類需要在單個方法的外部可見,或者太長了而不適合在一個方法內部,那麼使用成員類。如果成員類的每個例項需要它外部例項的引用,那麼把它變成非靜態;否則,把它變成靜態。假設類屬於一個方法的內部,如果你需要僅僅從一個位置建立例項,而且有描述這個類的特性的已存類,那麼把它變成匿名類;否則,把它變成本地類。

相關文章