設計模式(十六)----結構型模式之代理享元模式

2023-03-07 06:00:35

1 概述

定義:

運用共用技術來有效地支援大量細粒度物件的複用。它通過共用已經存在的物件來大幅度減少需要建立的物件數量、避免大量相似物件的開銷,從而提高系統資源的利用率。

2 結構

享元(Flyweight )模式中存在以下兩種狀態:

  1. 內部狀態,即不會隨著環境的改變而改變的可共用部分。

  2. 外部狀態,指隨環境改變而改變的不可以共用的部分。享元模式的實現要領就是區分應用中的這兩種狀態,並將外部狀態外部化。

享元模式的主要有以下角色:

  • 抽象享元角色(Flyweight):通常是一個介面或抽象類,在抽象享元類中宣告了具體享元類公共的方法,這些方法可以向外界提供享元物件的內部資料(內部狀態),同時也可以通過這些方法來設定外部資料(外部狀態)。

  • 具體享元(Concrete Flyweight)角色 :它實現了抽象享元類,稱為享元物件;在具體享元類中為內部狀態提供了儲存空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元物件。

  • 非享元(Unsharable Flyweight)角色 :並不是所有的抽象享元類的子類都需要被共用,不能被共用的子類可設計為非共用具體享元類;當需要一個非共用具體享元類的物件時可以直接通過範例化建立。

  • 享元工廠(Flyweight Factory)角色 :負責建立和管理享元角色。當客戶物件請求一個享元物件時,享元工廠檢査系統中是否存在符合要求的享元物件,如果存在則提供給客戶;如果不存在的話,則建立一個新的享元物件。

3 案例實現

【例】俄羅斯方塊

下面的圖片是眾所周知的俄羅斯方塊中的一個個方塊,如果在俄羅斯方塊這個遊戲中,每個不同的方塊都是一個範例物件,這些物件就要佔用很多的記憶體空間,下面利用享元模式進行實現。

先來看類圖:

程式碼如下:

俄羅斯方塊有不同的形狀,我們可以對這些形狀向上抽取出AbstractBox,用來定義共性的屬性和行為。

//  抽象享元角色
public abstract class AbstractBox {
    public abstract String getShape();
​
    public void display(String color) {
        System.out.println("方塊形狀:" + this.getShape() + " 顏色:" + color);
    }
}

接下來就是定義不同的形狀了,IBox類、LBox類、OBox類等。

//(具體享元角色)  
public class IBox extends AbstractBox {
​
    @Override
    public String getShape() {
        return "I";
    }
}
​
public class LBox extends AbstractBox {
​
    @Override
    public String getShape() {
        return "L";
    }
}
​
public class OBox extends AbstractBox {
​
    @Override
    public String getShape() {
        return "O";
    }
}

提供了一個工廠類(BoxFactory),用來管理享元物件(也就是AbstractBox子類物件),該工廠類物件只需要一個,所以可以使用單例模式。並給工廠類提供一個獲取形狀的方法。

//工廠類,將該類設計為單例
public class BoxFactory {
​
    private static HashMap<String, AbstractBox> map;
​
    //在構造方法中進行初始化操作
    private BoxFactory() {
        map = new HashMap<String, AbstractBox>();
        AbstractBox iBox = new IBox();
        AbstractBox lBox = new LBox();
        AbstractBox oBox = new OBox();
        map.put("I", iBox);
        map.put("L", lBox);
        map.put("O", oBox);
    }
​
    //提供一個方法獲取該工廠類物件
    public static final BoxFactory getInstance() {
        return SingletonHolder.INSTANCE;
    }
​
    private static class SingletonHolder {
        private static final BoxFactory INSTANCE = new BoxFactory();
    }
​
    public AbstractBox getBox(String key) {
        return map.get(key);
    }
}

測試類

public class Client {
    public static void main(String[] args) {
        //獲取I圖形物件
        AbstractBox box1 = BoxFactory.getInstance().getShape("I");
        box1.display("灰色");
​
        //獲取L圖形物件
        AbstractBox box2 = BoxFactory.getInstance().getShape("L");
        box2.display("綠色");
​
        //獲取O圖形物件
        AbstractBox box3 = BoxFactory.getInstance().getShape("O");
        box3.display("灰色");
​
        //獲取O圖形物件
        AbstractBox box4 = BoxFactory.getInstance().getShape("O");
        box4.display("紅色");
​
        System.out.println("兩次獲取到的O圖形物件是否是同一個物件:" + (box3 == box4));
    }
}
​

測試結果

4 優缺點和使用場景

1,優點

  • 極大減少記憶體中相似或相同物件數量,節約系統資源,提供系統效能

  • 享元模式中的外部狀態相對獨立,且不影響內部狀態,上面案例中的顏色,相同形狀不同顏色是同一個物件。

2,缺點:

為了使物件可以共用,需要將享元物件的部分狀態外部化,分離內部狀態和外部狀態,使程式邏輯複雜

3,使用場景:

  • 一個系統有大量相同或者相似的物件,造成記憶體的大量耗費,比如執行緒池或連線池。

  • 物件的大部分狀態都可以外部化,可以將這些外部狀態傳入物件中。

  • 在使用享元模式時需要維護一個儲存享元物件的享元池(上面案例中的hashmap),而這需要耗費一定的系統資源,因此,應當在需要多次重複使用享元物件時才值得使用享元模式。

5 JDK原始碼解析

Integer類使用了享元模式。我們先看下面的例子:

public class Demo {
    public static void main(String[] args) {
        // 自動裝箱
        Integer i1 = 127;
        Integer i2 = 127;
​
        System.out.println("i1和i2物件是否是同一個物件?" + (i1 == i2));
​
        Integer i3 = 128;
        Integer i4 = 128;
​
        System.out.println("i3和i4物件是否是同一個物件?" + (i3 == i4));
    }
}

執行上面程式碼,結果如下:

為什麼第一個輸出語句輸出的是true,第二個輸出語句輸出的是false?通過反編譯軟體進行反編譯,程式碼如下:

public class Demo {
    public static void main(String[] args) {
        Integer i1 = Integer.valueOf((int)127);
        Integer i2 Integer.valueOf((int)127);
        System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
        Integer i3 = Integer.valueOf((int)128);
        Integer i4 = Integer.valueOf((int)128);
        System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
    }
}

上面程式碼可以看到,直接給Integer型別的變數賦值基本資料型別資料的操作底層使用的是 valueOf() ,所以只需要看該方法即可

public final class Integer extends Number implements Comparable<Integer> {
    
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
​
        static {
            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) {
                }
            }
            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() {}
    }
}

可以看到 Integer 預設先建立並快取 -128 ~ 127 之間數的 Integer 物件,當呼叫 valueOf 時如果引數在 -128 ~ 127 之間則計算下標並從快取中返回,否則建立一個新的 Integer 物件。