Java設計模式之(十一)——享元模式

YSOcean發表於2021-11-30

1、什麼是享元模式?

Use sharing to support large numbers of fine-grained objects efficiently.

享元模式(Flyweight Pattern):使用共享物件可有效地支援大量的細粒度的物件。

說人話:複用物件,節省記憶體。

2、享元模式定義

image-20210916234023259

①、Flyweight——抽象享元角色

是一個產品的抽象類, 同時定義出物件的外部狀態和內部狀態的介面或實現。

一個物件資訊可以分為內部狀態和外部狀態。

內部狀態:物件可共享出來的資訊, 儲存在享元物件內部並且不會隨環境改變而改變,可以作為一個物件的動態附加資訊, 不必直接儲存在具體某個物件中, 屬於可以共享的部分。

外部狀態:物件得以依賴的一個標記, 是隨環境改變而改變的、 不可以共享的狀態。

②、ConcreteFlyweight——具體享元角色

具體的一個產品類, 實現抽象角色定義的業務。 該角色中需要注意的是內部狀態處理應該與環境無關, 不應該出現一個操作改變了內部狀態, 同時修改了外部狀態, 這是絕對不允許的。

③、unsharedConcreteFlyweight——不可共享的享元角色

不存在外部狀態或者安全要求(如執行緒安全) 不能夠使用共享技術的物件, 該物件一般不會出現在享元工廠中。

④、FlyweightFactory——享元工廠

職責非常簡單, 就是構造一個池容器, 同時提供從池中獲得物件的方法。

3、享元模式通用程式碼

/**
 * 抽象享元角色
 */
public abstract class Flyweight {
    // 內部狀態
    private String instrinsic;

    // 外部狀態 通過 final 修改,防止修改
    protected final String extrinsic;

    protected Flyweight(String extrinsic) {
        this.extrinsic = extrinsic;
    }

    // 定義業務操作
    public abstract void operate();

    public String getInstrinsic() {
        return instrinsic;
    }

    public void setInstrinsic(String instrinsic) {
        this.instrinsic = instrinsic;
    }
}
/**
 * 具體享元角色1
 */
public class ConcreteFlyweight1 extends Flyweight{

    protected ConcreteFlyweight1(String extrinsic) {
        super(extrinsic);
    }

    @Override
    public void operate() {
        System.out.println("具體享元角色1");
    }
}
/**
 * 具體享元角色2
 */
public class ConcreteFlyweight2 extends Flyweight{

    protected ConcreteFlyweight2(String extrinsic) {
        super(extrinsic);
    }

    @Override
    public void operate() {
        System.out.println("具體享元角色2");
    }
}
public class FlyweightFactory {
    // 定義一個池容器
    private static HashMap<String,Flyweight> pool = new HashMap<>();

    // 享元工廠
    public static Flyweight getFlyweight(String extrinsic){
        // 需要返回的物件
        Flyweight flyweight = null;
        // 池中沒有該物件
        if(pool.containsKey(extrinsic)){
            flyweight = pool.get(extrinsic);
        }else{
            // 根據外部狀態建立享元物件
            flyweight = new ConcreteFlyweight1(extrinsic);
            // 放置到池中
            pool.put(extrinsic,flyweight);
        }
        return flyweight;
    }
}

4、通過享元設計文字編輯器

假設文字編輯器只包含文字編輯功能,而且只記錄文字和格式兩部分資訊,其中格式包括文字的字型型號、大小、顏色等資訊。

4.1 普通實現

通常設計是把每個文字看成一個單獨物件。

package com.itcoke.designpattern.flyweight.edittext;

/**
 * 單個文字物件
 */
public class Character {
    // 字元
    private char c;
    // 字型型號
    private String font;
    // 字型大小
    private int size;
    // 字型顏色
    private int colorRGB;

    public Character(char c, String font, int size, int colorRGB){
        this.c = c;
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
    }

    @Override
    public String toString() {
        return String.valueOf(c);
    }
}

/**
 * 編輯器實現
 */
public class Editor {
    private ArrayList<Character> chars = new ArrayList<>();

    public void appendCharacter(char c, String font, int size, int colorRGB){
        Character character = new Character(c,font,size,colorRGB);
        chars.add(character);
    }

    public void display(){
        System.out.println(chars);
    }
}

客戶端:

public class EditorClient {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.appendCharacter('A',"宋體",11,0XFFB6C1);
        editor.appendCharacter('B',"宋體",11,0XFFB6C1);
        editor.appendCharacter('C',"宋體",11,0XFFB6C1);
        editor.display();
    }
}

4.2 享元模式改寫

上面的問題很容易發現,每一個字元就會建立一個 Character 物件,如果是幾百萬個字元,那記憶體中就會存在幾百萬的物件,那怎麼去節省這些記憶體呢?

其實,分析一下,對於字型的格式,通常不會有很多,於是我們可以把字型格式設定為享元,也就是上面說的可以共享的內部狀態。

內部狀態(共享):字型型別、大小、顏色

外部狀態(不共享):字元

於是程式碼改寫如下:

public class CharacterStyle {
    // 字型型號
    private String font;
    // 字型大小
    private int size;
    // 字型顏色
    private int colorRGB;

    public CharacterStyle(String font, int size, int colorRGB) {
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CharacterStyle that = (CharacterStyle) o;
        return size == that.size &&
                colorRGB == that.colorRGB &&
                Objects.equals(font, that.font);
    }

    @Override
    public int hashCode() {
        return Objects.hash(font, size, colorRGB);
    }
}
public class CharacterStyleFactory {

    private static final Map<CharacterStyle,CharacterStyle> mapStyles = new HashMap<>();

    public static CharacterStyle getStyle(String font, int size, int colorRGB){
        CharacterStyle newStyle = new CharacterStyle(font,size,colorRGB);

        if(mapStyles.containsKey(newStyle)){
            return mapStyles.get(newStyle);
        }
        mapStyles.put(newStyle,newStyle);
        return newStyle;
    }
}
public class Character {
    private char c;
    private CharacterStyle style;

    public Character(char c, CharacterStyle style) {
        this.c = c;
        this.style = style;
    }

    @Override
    public String toString() {
        return String.valueOf(c);
    }
}
public class Editor {
    private List<Character> chars = new ArrayList<>();

    public void appendCharacter(char c, String font, int size, int colorRGB){
        Character character = new Character(c,CharacterStyleFactory.getStyle(font,size,colorRGB));
        chars.add(character);
    }

    public void display(){
        System.out.println(chars);
    }
}

5、享元模式在 java.lang.Integer 中應用

看下面這段程式碼,列印結果是啥?

public class IntegerTest {
    public static void main(String[] args) {
        Integer i1 = 56;
        Integer i2 = 56;
        Integer i3 = 129;
        Integer i4 = 129;
        System.out.println(i1 == i2); 
        System.out.println(i3 == i4); 
    }
}

image-20210917221539913

為什麼是這種結果呢?

首先說一下 Integer i = 59;底層執行了:Integer i = Integer.valueOf(59); 這是自動裝箱。

int j = i; 底層執行了:int j = i.intValue(); 這是自動拆箱。

然後我們Integer.valueOf() 方法:

image-20210917222245550

再看 IntegerCache 原始碼:

   private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

其實這就是我們前面說的享元物件的工廠類,快取 -128 到 127 之間的整型值,這是最常用的一部分整型值,當然JDK 也提供了方法來讓我們可以自定義快取的最大值。

6、享元模式優點

減少應用程式建立的物件, 降低程式記憶體的佔用, 增強程式的效能。

但它同時也提高了系統複雜性, 需要分離出外部狀態和內部狀態, 而且外部狀態具有固化特性, 不應該隨內部狀態改變而改變, 否則導致系統的邏輯混亂。

7、享元模式應用場景

①、系統中存在大量的相似物件。

②、細粒度的物件都具備較接近的外部狀態, 而且內部狀態與環境無關, 也就是說物件沒有特定身份。

③、需要緩衝池的場景。

相關文章