Flyweight

December 30, 2021

你現在打算建立一個文書處理程式,文件中的文字可以設置字型資訊,也許是這麼設定一段文字的字型資訊:

text.setFont(new Font("細明體", Style.BOLD, 12));

文書處理程式中的文字都會有字型資訊,許多字型資訊往往是重複的,例如有些文字都是細明體、粗體、大小 12 的字型,有些文字都是標楷體、斜體、大小 16 的字型,對於同樣字型資訊的文字,卻每次都用新建字型物件的方式來設定,雖然是小物件,但數量多的話,也有可能佔用相當的記憶體。

小物件的重用

對於小且可以資訊重複的物件,可以考慮共用,例如:

enum Style {PLAIN, BOLD, ITALIC}

record Font(String name, Style style, int size) {}

class FontFactory {
    private static Map<Font, WeakReference<Font>> fonts =
        new WeakHashMap<>();
       
    static Font get(String name, Style style, int size) {
    	// 不在意建立這小東西,只在意全部記憶體用量
        Font font = new Font(name, style, size);
        if(!fonts.containsKey(font)) {
            fonts.put(font, new WeakReference<Font>(font));
        }
        return fonts.get(font).get();
    }
}

public class Main {
    public static void main(String[] args) {
        Font font1 = FontFactory.get("細明體", Style.BOLD, 12);
        Font font2 = FontFactory.get("細明體", Style.BOLD, 12);
        out.println(font1 == font2);
    }
}

這就是 Flyweight 模式,簡單來說,就是對客戶端隱藏小物件的建立、共用等細節,因此經常搭配〈Simple Factory〉實現。

範例程式中使用的 WeakHashMap,可在記憶體不足時,主動釋放未被程式其他部份參考的物件,至於 record 則是 Java 16 以後的新型態,它有一個特性,就是狀態無法變動(immutable),畢竟會被共用的物件,狀態要是會變動的話,會惹來許多麻煩。

就這麼簡單?這不是快取嗎?就這麼簡單,要說是快取也對!只不過既然名為 Flyweight,表示這種模式通常用於小物件的共用,Gof 的著眼點也僅在於記憶體的用量,通常提及快取,可能考量的東西會更多,像是快取能否提升系統效能之類的…Flyweight 倒是沒想那麼多…XD

為什麼 Flyweight 被 Gof 放在結構分類呢?像 Font 這種被共用的角色,會被組裝到很多地方,從共用物件的來源,如何銜接至目的地來看這個模式,因此才分到結構吧!

Java 的 Flyweight 實現

Java 中 Flyweight 模式的實際應用之一是 Integer 之類,透過 Integer.valueOf 取得的實例,在一定範圍內的話,會是相同實例:

public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

有時不見得是透過 factory 之類的方法來取得物件,例如字串,在 Java 建立的字串,實際上會是同一物件:

String str1 = "flyweight";
String str2 = "flyweight";
out.println(str1 == str2);

程式的執行結果會顯示 true,因為 JVM 會維護一個字串池(String Pool),以上的寫法在字串池中查找是否存在相同內容的實例,如果有就直接傳回,而不是直接創造一個新的 String 實例,以減少記憶體的耗用。