第24條
靜態成員類優于非靜態成員類
嵌套類(nested class)是指定義在另一個類的內部的類。嵌套類存在的目的應該只是為它的外圍類(enclosing class)提供服務。如果嵌套類將來可能會用于其他的某個環境中,它就應該是頂層類(top-level class)。嵌套類有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)和局部類(local class)。除了第一種之外,其他三種都稱為內部類(inner class) 。本條目將告訴你什么時候應該使用哪種嵌套類,以及這樣做的原因。
靜態成員類是最簡單的一種嵌套類。最好把它看作是普通的類,只是碰巧被聲明在另一個類的內部而已,它可以訪問外圍類的所有成員,包括那些聲明為私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。如果它被聲明為私有的,它就只能在外圍類的內部才可以被訪問,等等。
靜態成員類的一種常見用法是作為公有的輔助類,只有與它的外部類一起使用才有意義。例如,以枚舉為例,它描述了計算器支持的各種操作(詳見第34條)。Operation枚舉應該是Calculator類的公有靜態成員類,之后Calculator類的客戶端就可以用諸如 Calculator.Operation.PLUS 和 Calculator.Operation.MINUS 這樣的名稱來引用這些操作。
從語法上講,靜態成員類和非靜態成員類之間唯一的區別是,靜態成員類的聲明中包含修飾符static。盡管它們的語法非常相似,但是這兩種嵌套類有很大的不同。非靜態成員類的每個實例都隱含地與外圍類的一個外圍實例(enclosing instance)相關聯。在非靜態成員類的實例方法內部,可以調用外圍實例上的方法,或者利用修飾過的this(qualified this)構造獲得外圍實例的引用。如果嵌套類的實例可以在它外圍類的實例之外獨立存在,這個嵌套類就必須是靜態成員類:在沒有外圍實例的情況下,要想創建非靜態成員類的實例是不可能的。
當非靜態成員類的實例被創建的時候,它和外圍實例之間的關聯關系也隨之被建立起來;而且,這種關聯關系以后不能被修改。通常情況下,當在外圍類的某個實例方法的內部調用非靜態成員類的構造器時,這種關聯關系被自動建立起來。使用表達式enclosingInstance.newMemberClass(args)來手工建立這種關聯關系也是有可能的,但是很少使用。正如你所預料的那樣,這種關聯關系需要消耗非靜態成員類實例的空間,并且會增加構造的時間開銷。
非靜態成員類的一種常見用法是定義一個Adapter,它允許外部類的實例被看作是另一個不相關的類的實例。例如,Map接口的實現往往使用非靜態成員類來實現它們的集合視圖(collection view),這些集合視圖是由Map的keySet、entrySet和values方法返回的。同樣地,諸如Set和List這種集合接口的實現往往也使用非靜態成員類來實現它們的迭代器(iterator):
// Typical use of a nonstatic member classpublic class MySet<E> extends AbstractSet<E> { ... // Buik of the class omitted ?Override public Iterator iterator() ( return new Mylterator(); } private class Mylterator implements Iterator<E> { }}
如果聲明成員類不要求訪問外圍實例,就要始終把修飾符static放在它的聲明中,使它成為靜態成員類,而不是非靜態成員類。如果省略了static修飾符,則每個實例都將包含一個額外的指向外圍對象的引用。如前所述,保存這份引用要消耗時間和空間,并且會導致外圍實例在符合垃圾回收(詳見第7條)時卻仍然得以保留。由此造成的內存泄漏可能是災難性的。但是常常難以發現,因為這個引用是不可見的。
私有靜態成員類的一種常見用法是代表外圍類所代表的對象的組件。以Map實例為例,它把鍵(key)和值(value)關聯起來。許多Map實現的內部都有一個Entry對象,對應于Map中的每個鍵-值對。雖然每個entry都與一個Map關聯,但是entry上的方法(getKey、getValue和setValue)并不需要訪問該Map。因此,使用非靜態成員類來表示entry是很浪費的:私有的靜態成員類是最佳的選擇。如果不小心漏掉了entry聲明中的static修飾符,該Map仍然可以工作,但是每個entry中將會包含一個指向該Map的引用,這樣就浪費了空間和時間。
如果相關的類是導出類的公有或受保護的成員,毫無疑問,在靜態和非靜態成員類之間做出正確的選擇是非常重要的。在這種情況下,該成員類就是導出的API元素,在后續的發行版本中,如果不違反向后兼容性,就無法從非靜態成員類變為靜態成員類。
顧名思義,匿名類是沒有名字的。它不是外圍類的一個成員。它并不與其他的成員一起被聲明,而是在使用的同時被聲明和實例化。匿名類可以出現在代碼中任何允許存在表達式的地方。當且僅當匿名類出現在非靜態的環境中時,它才有外圍實例。但是即使它們出現在靜態的環境中,也不可能擁有任何靜態成員,而是擁有常數變量(constant variable),常數變量是final基本類型,或者被初始化成常量表達式的字符串域。
匿名類的運用受到諸多的限制。除了在它們被聲明的時候之外,是無法將它們實例化的。不能執行instanceof測試,或者做任何需要命名類的其他事情。無法聲明一個匿名類來實現多個接口,或者擴展一個類,并同時擴展類和實現接口。除了從超類型中繼承得到之外,匿名類的客戶端無法調用任何成員。由于匿名類出現在表達式中,它們必須保持簡短(大約10行或者更少),否則會影響程序的可讀性。
在Java中增加lambda(詳見第6章)之前,匿名類是動態地創建小型函數對象(function object)和過程對象(process object)的最佳方式,但是現在會優先選擇lambda(詳見第42條)。匿名類的另一種常見用法是在靜態工廠方法的內部(參見第20條中的intArrayAsList方法)。
局部類是四種嵌套類中使用最少的類。在任何“可以聲明局部變量”的地方,都可以聲明局部類,并且局部類也遵守同樣的作用域規則。局部類與其他三種嵌套類中的每一種都有一些共同的屬性。與成員類一樣,局部類有名字,可以被重復使用。與匿名類一樣,只有當局部類是在非靜態環境中定義的時候,才有外圍實例,它們也不能包含靜態成員。與匿名類一樣,它們必須非常簡短,以便不會影響可讀性。
總而言之,共有四種不同的嵌套類,每一種都有自己的用途。如果一個嵌套類需要在單個方法之外仍然是可見的,或者它太長了,不適合放在方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態的;否則,就做成靜態的。假設這個嵌套類屬于一個方法的內部,如果你只需要在一個地方創建實例,并且已經有了一個預置的類型可以說明這個類的特征,就要把它做成匿名類;否則,就做成局部類。
掃描二維碼
獲取更多精彩
Java樂分享

?感謝支持