Effective Java Item24:比起非靜態成員類型,更偏好靜態成員類型
整理 Effective Java 書中 Item 24: Prefer static member classes to non-static member classes 心得筆記
主旨
在 Java 中,巢狀類別(Nested Class)是一種將類別定義在另一個類別內部的設計方式。根據是否需要外部類別的實例,有四種巢狀類別:static member class、non-static member class、local class 和 anonymous class。本篇聚焦在:當巢狀類別不需要外部類別實例時,應優先使用 static member class,這樣可以節省記憶體、提升效能、避免記憶體洩漏,也對設計更有彈性。
點出問題:你可能不小心造成記憶體洩漏
很多人會寫出像這樣的內部類別:
public class Outer {
class Inner {
void hello() {
System.out.println("Hi!");
}
}
}
這段看起來沒問題,但 Inner 是非靜態類別,代表它 會自動持有一個指向 Outer 實例的隱藏參考。只要 Inner 還在記憶體中,Outer 就無法被 GC 回收。
如果你根本不需要 Inner 使用 Outer 的資料,這樣的隱藏參考就等於白白浪費記憶體,甚至導致 記憶體洩漏。
範例:正確的 static member class 寫法
假設你寫一個map類別,裡面每組 key-value 對應的資料用一個 Entry 表示。這個 Entry 不需要知道整張 map 的狀態,就可以獨立運作。
這時應該這樣寫:
public class MyMap {
private static class Entry<K, V> {
final K key;
V value;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
V getValue() {
return value;
}
void setValue(V newValue) {
value = newValue;
}
}
}
因為加了 static,這個 Entry 就不會默默持有外部類別 MyMap 的參考。這樣的設計更有效率,也不會製造多餘的相依性。
劃重點:何時該用 static?何時不用?
| 類型 | 是否需 Outer instance | 適用情境 |
|---|---|---|
static member class | 否 | 若內部類別能獨立運作,例如 Entry, Operation, Node 等 |
non-static member class | 是 | 需要訪問外部類別的狀態,例如 Iterator 要操作外部資料結構 |
local class | 否/是 | 僅在方法內短暫使用時 |
anonymous class | 否/是 | 一次性行為,如事件處理、函式物件 |
範例:快取資料結構設計
這裡設計一個簡單的快取類別 SimpleCache,裡面用一個巢狀類別 CacheEntry 來表示每一筆快取資料。每筆資料有 value 和 expiresAt,方便過期清除:
public class SimpleCache {
private final Map<String, CacheEntry> cache = new HashMap<>();
private static class CacheEntry {
Object value;
long expiresAt;
CacheEntry(Object value, long ttlMillis) {
this.value = value;
this.expiresAt = System.currentTimeMillis() + ttlMillis;
}
boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
}
public void put(String key, Object value, long ttlMillis) {
cache.put(key, new CacheEntry(value, ttlMillis));
}
public Object get(String key) {
CacheEntry entry = cache.get(key);
if (entry == null || entry.isExpired()) {
cache.remove(key);
return null;
}
return entry.value;
}
}
真實世界示範
假設我們要放入大量快取資料:
public class CacheTest {
public static void main(String[] args) {
SimpleCache cache = new SimpleCache();
// 放入 100,000 筆資料,每筆 5 分鐘過期
for (int i = 0; i < 100_000; i++) {
cache.put("key" + i, "value" + i, 5 * 60 * 1000);
}
System.out.println(cache.get("key99999")); // 應該拿得到
}
}
如果你把 CacheEntry 改成 非 static 類別,每一筆資料都會不小心帶著一份 SimpleCache 的參考。這表示:
- 原本早就不會再用到的
SimpleCache物件,會因為某筆CacheEntry被外部誤引用,而永遠無法回收。 - 這種記憶體洩漏很難發現,因為 reference 是「隱藏的」。
這就是為什麼只要你的內部類別不需要存取外部類別實例,就應該 果斷加上 static。
小結
- 如果巢狀類別不需要存取外部類別的資料或方法,請一律使用
static。 - 使用 non-static 會自動建立對外部實例的參考,可能會浪費記憶體,甚至導致 記憶體洩漏。
- 若該巢狀類別是 API 的一部分,一開始就設計成
static更好,未來才能維持相容性。 - 巢狀類別的選擇不只是語法問題,而是設計品質與效能的關鍵。