整理 Effective Java 書中 Item 24: Prefer static member classes to non-static member classes 心得筆記

主旨

在 Java 中,巢狀類別(Nested Class)是一種將類別定義在另一個類別內部的設計方式。根據是否需要外部類別的實例,有四種巢狀類別:static member classnon-static member classlocal classanonymous 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 來表示每一筆快取資料。每筆資料有 valueexpiresAt,方便過期清除:

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 更好,未來才能維持相容性。
  • 巢狀類別的選擇不只是語法問題,而是設計品質與效能的關鍵。