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
實例,以減少記憶體的耗用。