Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
嵌套類(nested class)是在另一個類中定義的類。 嵌套類應該只存在於其宿主類(enclosing class)中。 如果一個嵌套類在其他一些情況下是有用的,那么它應該是一個頂級類。 有四種嵌套類:靜態成員類,非靜態成員類,匿名類和局部類。 除了第一種以外,剩下的三種都被稱為內部類(inner class)。 這個條目告訴你什么時候使用哪種類型的嵌套類以及為什么使用。
靜態成員類是最簡單的嵌套類。 最好把它看作是一個普通的類,恰好在另一個類中聲明,並且可以訪問所有宿主類的成員,甚至是那些被聲明為私有類的成員。 靜態成員類是其宿主類的靜態成員,並遵循與其他靜態成員相同的可訪問性規則。 如果它被聲明為private,則只能在宿主類中訪問,等等。
靜態成員類的一個常見用途是作為公共幫助類,僅在與其外部類一起使用時才有用。 例如,考慮一個描述計算器支持的操作的枚舉類型(條目 34)。 Operation
枚舉應該是Calculator
類的公共靜態成員類。 Calculator
客戶端可以使用Calculator.Operation.PLUS
和Calculator.Operation.MINUS
等名稱來引用操作。
在語法上,靜態成員類和非靜態成員類之間的唯一區別是靜態成員類在其聲明中具有static修飾符。 盡管句法相似,但這兩種嵌套類是非常不同的。 非靜態成員類的每個實例都隱含地與其包含的類的宿主實例相關聯。 在非靜態成員類的實例方法中,可以調用宿主實例上的方法,或者使用限定的構造[JLS,15.8.4]獲得對宿主實例的引用。 如果嵌套類的實例可以與其宿主類的實例隔離存在,那么嵌套類必須是靜態成員類:不可能在沒有宿主實例的情況下創建非靜態成員類的實例。
非靜態成員類實例和其宿主實例之間的關聯是在創建成員類實例時建立的,並且之后不能被修改。 通常情況下,通過在宿主類的實例方法中調用非靜態成員類構造方法來自動建立關聯。 盡管很少有可能使用表達式enclosingInstance.new MemberClass(args)
手動建立關聯。 正如你所預料的那樣,該關聯在非靜態成員類實例中占用了空間,並為其構建添加了時間開銷。
非靜態成員類的一個常見用法是定義一個Adapter [Gamma95],它允許將外部類的實例視為某個不相關類的實例。 例如,Map接口的實現通常使用非靜態成員類來實現它們的集合視圖,這些視圖由Map的keySet
,entrySet
和values
方法返回。 同樣,集合接口(如Set和List)的實現通常使用非靜態成員類來實現它們的迭代器:
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
@Override public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
}
如果你聲明了一個不需要訪問宿主實例的成員類,總是把static修飾符放在它的聲明中,使它成為一個靜態成員類,而不是非靜態的成員類。 如果你忽略了這個修飾符,每個實例都會有一個隱藏的外部引用給它的宿主實例。 如前所述,存儲這個引用需要占用時間和空間。 更嚴重的是,並且會導致即使宿主類在滿足垃圾回收的條件時卻仍然駐留在內存中(條目 7)。 由此產生的內存泄漏可能是災難性的。 由於引用是不可見的,所以通常難以檢測到。
私有靜態成員類的常見用法是表示由它們的宿主類表示的對象的組件。 例如,考慮將鍵與值相關聯的Map實例。 許多Map實現對於映射中的每個鍵值對都有一個內部的Entr
y對象。 當每個entry
都與Map關聯時,entry
上的方法(getKey
,getValue
和setValue
)不需要訪問Map。 因此,使用非靜態成員類來表示entry將是浪費的:私有靜態成員類是最好的。 如果意外地忽略了entry
聲明中的static修飾符,Map仍然可以工作,但是每個entry都會包含對Map的引用,浪費空間和時間。
如果所討論的類是導出類的公共或受保護成員,則在靜態和非靜態成員類之間正確選擇是非常重要的。 在這種情況下,成員類是導出的API元素,如果不違反向后兼容性,就不能在后續版本中從非靜態變為靜態成員類。
正如你所期望的,一個匿名類沒有名字。 它不是其宿主類的成員。 它不是與其他成員一起聲明,而是在使用時同時聲明和實例化。 在表達式合法的代碼中,匿名類是允許的。 當且僅當它們出現在非靜態上下文中時,匿名類才會封裝實例。 但是,即使它們出現在靜態上下文中,它們也不能有除常量型變量之外的任何靜態成員,這些常量型變量包括final的基本類型,或者初始化常量表達式的字符串屬性[JLS,4.12.4]。
匿名類的適用性有很多限制。 除了在聲明的時候之外,不能實例化它們。 你不能執行instanceof
方法測試或者做任何其他需要你命名的類。 不能聲明一個匿名類來實現多個接口,或者繼承一個類並同時實現一個接口。 匿名類的客戶端不能調用除父類型繼承的成員以外的任何成員。 因為匿名類在表達式中出現,所以它們必須保持短——約十行或更少——否則可讀性將受損。
在將lambda表達式添加到Java(第6章)之前,匿名類是創建小方法對象和處理對象的首選方法,但lambda表達式現在是首選(條目 42)。 匿名類的另一個常見用途是實現靜態工廠方法(請參閱條目 20中的intArrayAsList)。
局部類是四種嵌套類中使用最少的。 一個局部類可以在任何可以聲明局部變量的地方聲明,並遵守相同的作用域規則。 局部類與其他類型的嵌套類具有共同的屬性。 像成員類一樣,他們有名字,可以重復使用。 就像匿名類一樣,只有在非靜態上下文中定義它們時,它們才會包含實例,並且它們不能包含靜態成員。 像匿名類一樣,應該保持簡短,以免損害可讀性。
回顧一下,有四種不同的嵌套類,每個都有它的用途。 如果一個嵌套的類需要在一個方法之外可見,或者太長而不能很好地適應一個方法,使用一個成員類。 如果一個成員類的每個實例都需要一個對其宿主實例的引用,使其成為非靜態的; 否則,使其靜態。 假設這個類屬於一個方法內部,如果你只需要從一個地方創建實例,並且存在一個預置類型來說明這個類的特征,那么把它作為一個匿名類; 否則,把它變成局部類。