享元模式

定義

享元(Flyweight)模式又稱蠅量模式,主要運用共享技術來有效地支持大量細粒度對象的複用。主要用於減少創建對象的數量,以減少內存佔用和提高性能。

如果想要讓某個類的一個實例用來提供許多 “虛擬實例” ,就可以考慮使用享元模式。

享元模式屬於對象結構型模式。


要點

優點:

  • 可以極大減少內存中對象的數量,使得相同或相似對象在內存中只保存一份,從而可以節約系統資源,提高系統性能。
  • 享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元對象可以在不同的環境中被共享。

缺點:

  • 一旦使用享元模式,那麼單個的邏輯實例將無法擁有獨立而不同的行爲。
  • 享元模式使得系統變得複雜,需要分離出內部狀態和外部狀態,這使得程序的邏輯複雜化。
  • 爲了使對象可以共享,享元模式需要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間變長。

享元模式的主要角色有:
抽象享元(Flyweight):是所有的具體享元類的基類,爲具體享元規範需要實現的公共接口,非享元的外部狀態以參數的形式通過方法傳入。
具體享元 (Concrete Flyweight):實現抽象享元角色中所規定的接口。
非享元(Unsharable Flyweight):是不可以共享的外部狀態,它以參數的形式注入具體享元中。
享元工廠(Flyweight Factory):負責創建和管理享元角色。它提供一個用於存儲享元對象的享元池,當用戶需要對象時,首先從享元池中獲取,如果享元池中不存在,則創建一個新的享元對象返回給用戶,並在享元池中保存該新增對象。
在這裏插入圖片描述


場景

某公司需要開發一款內網網盤,該網盤對於相同的文件只保留一份,譬如當上傳一部別人上傳過的文件,即使修改了文件名,只要文件內容一致,會發現很快就上傳完成了,實際上不是真的上傳,而是引用別人曾經上傳過的那份文件,不但可以提高用戶體驗,還可以節約存儲空間避免資源浪費。


實現

Resource(享元內部狀態)

/**
 * 資源類 Resource,相當於享元類的內部狀態
 */
public class Resource {

    /**
     * 文件資源唯一ID
     */
    private String hashId;
    /**
     * 文件大小
     */
    private long byteSize;
    /**
     * 文件內容
     */
    private byte[] content;

    public Resource(String hashId, long byteSize, byte[] content) {
        this.hashId = hashId;
        this.byteSize = byteSize;
        this.content = content;
    }

    public String getHashId() {
        return hashId;
    }

    public long getByteSize() {
        return byteSize;
    }

    public byte[] getContent() {
        return content;
    }

    @Override
    public String toString() {
        return "{ 資源ID=" + hashId +
                ", 文件大小=" + byteSize +
                " bytes }";
    }
}

UserFile(享元類)

/**
 * 用戶的文件類,相當於具體享元類(ConcreteFlyweight)
 *
 * 其中的 resource 爲內部狀態
 * owner 和 filename爲外部狀態,也就是非享元(UnsharedConcreteFlyWeight),外部狀態不可共享
 */
public class UserFile {

    /**
     * 文件名
     */
    private String filename;
    /**
     * 用戶
     */
    private String owner;
    /**
     * 文件資源信息
     */
    private Resource resource;

    public UserFile(String owner, String filename, Resource resource) {
        this.owner = owner;
        this.filename = filename;
        this.resource = resource;
    }

    public String getFilename() {
        return filename;
    }

    public String getOwner() {
        return owner;
    }

    public Resource getResource() {
        return resource;
    }

    @Override
    public String toString() {
        return "" +
                "文件名:" + filename +
                ", 用戶:" + owner +
                ", 資源信息:" + resource;
    }
}

PanServer(享元工廠)

/**
 * 網盤服務,相當於享元工廠(FlyWeightFactory)
 */
public class PanServer {

    /**
     * 單例模式 - 網盤服務
     */
    private static PanServer server = new PanServer();
    /**
     * 文件資源池,相當於享元池,只保留不同的文件資源 (hashId : Resource)
     */
    private Map<String, Resource> resourceSystem;
    /**
     * 用戶文件列表 (filename : UserFile)
     */
    private Map<String, UserFile> fileSystem;
    /**
     * 文件上傳位置
     */
    private final String DEST_FILEPATH = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/codedancing/designpattern/structural/flyweight/files/";

    private PanServer() {
        resourceSystem = new HashMap<>();
        fileSystem = new HashMap<>();
    }

    public static PanServer getInstance() {
        return server;
    }

    /**
     * 上傳文件
     */
    public void upload(String username, String fileName, String filePath) {
        long startTime = System.currentTimeMillis();
        System.out.println("準備上傳文件: " + fileName);

        File source = new File(filePath);
        File dest = new File(DEST_FILEPATH + fileName);
        if (dest.exists()) {
            System.out.println("文件名稱已存在,請重新上傳\n\n");
            return;
        }
        // 檢測文件,利用文件內容計算文件的唯一ID
        String hashId = HashUtil.computeHashId(ReadFileUtil.readFileToBytes(filePath));

        try {
            if (resourceSystem.containsKey(hashId)) {
                System.out.println(String.format("Server:檢測到內容相同的文件 [ %s ] ,爲了節約空間,重用文件", fileName));
                // 取出資源池中重複的文件資源
                Resource resource = resourceSystem.get(hashId);
                // 保存新文件至用戶文件
                fileSystem.put(fileName, new UserFile(username, fileName, resource));
                Thread.sleep(100);
            } else {
                System.out.println("正在上傳文件: " + fileName + " ...");
                // 開始上傳
                uploadFileToServer(source, dest);
                // 添加文件資源到資源池
                Resource resource = new Resource(hashId, dest.length(), ReadFileUtil.readFileToBytes(DEST_FILEPATH + fileName));
                resourceSystem.put(hashId, resource);
                fileSystem.put(fileName, new UserFile(username, fileName, resource));
                // 上傳文件需要耗費一定時間
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println(String.format("文件上傳完成,共耗費 %s 毫秒\n\n", endTime - startTime));
    }


    /**
     * 上傳指定文件到指定目錄
     */
    private void uploadFileToServer(File source, File dest) {
        FileChannel inputChannel = null;
        FileChannel outputChannel = null;

        try {
            inputChannel = new FileInputStream(source).getChannel();
            outputChannel = new FileOutputStream(dest).getChannel();
            outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputChannel != null) {
                    inputChannel.close();
                }
                if (outputChannel != null) {
                    outputChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 下載文件(模擬)
     */
    public void download(String fileName) {
        UserFile file = fileSystem.get(fileName);
        if (file == null) {
            System.out.println("文件不存在");
        } else {
            System.out.println("下載文件 --- " + file);
        }
    }

}

Client

public class Client {

    public static void main(String[] args) {

        PanServer server = PanServer.getInstance();

        // 上傳文件
        System.out.println("=================上傳文件===================");
        String filePath = "/home/codedancing/Desktop/HeadFirst設計模式.pdf";
        String filePath1 = "/home/codedancing/Desktop/朱一旦的HeadFirst設計模式.pdf";
        String filePath2 = "/home/codedancing/Desktop/Test-1.0-SNAPSHOT.war";

        server.upload("朱一旦", "HeadFirst設計模式.pdf", filePath);
        server.upload("朱一旦", "HeadFirst設計模式.pdf", filePath);
        server.upload("朱一旦", "朱一旦的HeadFirst設計模式.pdf", filePath1);
        server.upload("朱一旦", "Test-1.0-SNAPSHOT.war", filePath2);

        // 下載文件
        System.out.println("=================下載文件===================");
        server.download("HeadFirst設計模式.pdf");
        server.download("HeadFirst設計模式.pdf");
        server.download("朱一旦的HeadFirst設計模式.pdf");
        server.download("Test-1.0-SNAPSHOT.war");

    }

}


-------------------輸出---------------------

=================上傳文件===================
準備上傳文件: HeadFirst設計模式.pdf
正在上傳文件: HeadFirst設計模式.pdf ...
文件上傳完成,共耗費 3033 毫秒


準備上傳文件: HeadFirst設計模式.pdf
文件名稱已存在,請重新上傳


準備上傳文件: 朱一旦的HeadFirst設計模式.pdf
Server:檢測到內容相同的文件 [ 朱一旦的HeadFirst設計模式.pdf ] ,爲了節約空間,重用文件
文件上傳完成,共耗費 103 毫秒


準備上傳文件: Test-1.0-SNAPSHOT.war
正在上傳文件: Test-1.0-SNAPSHOT.war ...
文件上傳完成,共耗費 3049 毫秒


=================下載文件===================
下載文件 --- 文件名:HeadFirst設計模式.pdf, 用戶:朱一旦, 資源信息:{ 資源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下載文件 --- 文件名:HeadFirst設計模式.pdf, 用戶:朱一旦, 資源信息:{ 資源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下載文件 --- 文件名:朱一旦的HeadFirst設計模式.pdf, 用戶:朱一旦, 資源信息:{ 資源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下載文件 --- 文件名:Test-1.0-SNAPSHOT.war, 用戶:朱一旦, 資源信息:{ 資源ID=b0e5be44bdbbe8705e873a13504b958d, 文件大小=5839999 bytes }

Process finished with exit code 0

源碼

Click here


總結

  1. 系統中存在大量相同或相似的對象,這些對象耗費大量的內存資源,可以考慮使用享元模式。
  2. 大部分的對象可以按照內部狀態進行分組,且可將不同部分外部化,這樣每一個組只需保存一個內部狀態。
  3. 由於享元模式需要額外維護一個保存享元的數據結構,所以應當在有足夠多的享元實例時才值得使用享元模式。

JDK中的String類使用了享元模式
Java中的字符串一般保存在字符串常量池中,java會確保一個字符串在常量池中只有一個拷貝,這個字符串常量池在JDK6.0以前是位於常量池中,位於永久代,而在JDK7.0中,JVM將其從永久代拿出來放置於堆中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章