設計模式 | 享元模式及典型應用

前言

本文的主要內容:html

  • 介紹享元模式
  • 示例-雲盤
  • 總結
  • 源碼分析享元模式的典型應用
    • String中的享元模式
    • Integer中的享元模式
    • Long中的享元模式
    • Apache Common Pool2中的享元模式

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用
設計模式 | 適配器模式及典型應用java

點擊[閱讀原文]可訪問個人我的博客:laijianfeng.org數據庫

關注【小旋鋒】微信公衆號


享元模式

享元模式(Flyweight Pattern):運用共享技術有效地支持大量細粒度對象的複用。系統只使用少許的對象,而這些對象都很類似,狀態變化很小,能夠實現對象的屢次複用。因爲享元模式要求可以共享的對象必須是細粒度對象,所以它又稱爲輕量級模式,它是一種對象結構型模式。享元模式結構較爲複雜,通常結合工廠模式一塊兒使用。編程

角色

Flyweight(抽象享元類):一般是一個接口或抽象類,在抽象享元類中聲明瞭具體享元類公共的方法,這些方法能夠向外界提供享元對象的內部數據(內部狀態),同時也能夠經過這些方法來設置外部數據(外部狀態)。設計模式

ConcreteFlyweight(具體享元類):它實現了抽象享元類,其實例稱爲享元對象;在具體享元類中爲內部狀態提供了存儲空間。一般咱們能夠結合單例模式來設計具體享元類,爲每個具體享元類提供惟一的享元對象。數組

UnsharedConcreteFlyweight(非共享具體享元類):並非全部的抽象享元類的子類都須要被共享,不能被共享的子類可設計爲非共享具體享元類;當須要一個非共享具體享元類的對象時能夠直接經過實例化建立。緩存

FlyweightFactory(享元工廠類):享元工廠類用於建立並管理享元對象,它針對抽象享元類編程,將各類類型的具體享元對象存儲在一個享元池中,享元池通常設計爲一個存儲「鍵值對」的集合(也能夠是其餘類型的集合),能夠結合工廠模式進行設計;當用戶請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已建立的實例或者建立一個新的實例(若是不存在的話),返回新建立的實例並將其存儲在享元池中。安全

單純享元模式:在單純享元模式中,全部的具體享元類都是能夠共享的,不存在非共享具體享元類。
複合享元模式:將一些單純享元對象使用組合模式加以組合,還能夠造成複合享元對象,這樣的複合享元對象自己不能共享,可是它們能夠分解成單純享元對象,然後者則能夠共享bash

在享元模式中引入了享元工廠類,享元工廠類的做用在於提供一個用於存儲享元對象的享元池,當用戶須要對象時,首先從享元池中獲取,若是享元池中不存在,則建立一個新的享元對象返回給用戶,並在享元池中保存該新增對象。微信

典型的享元工廠類的代碼以下:

class FlyweightFactory {
    //定義一個HashMap用於存儲享元對象,實現享元池
    private HashMap flyweights = newHashMap();
    public Flyweight getFlyweight(String key){
        //若是對象存在,則直接從享元池獲取
        if(flyweights.containsKey(key)){
            return(Flyweight)flyweights.get(key);
        }
        //若是對象不存在,先建立一個新的對象添加到享元池中,而後返回
        else {
            Flyweight fw = newConcreteFlyweight();
            flyweights.put(key,fw);
            return fw;
        }
    }
}
複製代碼

享元類的設計是享元模式的要點之一,在享元類中要將內部狀態和外部狀態分開處理,一般將內部狀態做爲享元類的成員變量,而外部狀態經過注入的方式添加到享元類中。

典型的享元類代碼以下所示:

class Flyweight {
    //內部狀態intrinsicState做爲成員變量,同一個享元對象其內部狀態是一致的
    private String intrinsicState;
    public Flyweight(String intrinsicState) {
        this.intrinsicState=intrinsicState;
    }
    //外部狀態extrinsicState在使用時由外部設置,不保存在享元對象中,即便是同一個對象
    public void operation(String extrinsicState) {
        //......
    }
}
複製代碼

享元模式通常的類圖以下

享元模式類圖

示例

通常網盤對於相同的文件只保留一份,譬若有一個場景:當咱們上傳一部別人上傳過的電影,會發現很快就上傳完成了,實際上不是真的上傳,而是引用別人曾經上傳過的那部電影,這樣一能夠提升咱們的用戶體驗,二能夠節約存儲空間避免資源浪費

注意:這個場景是小編想的,與通常見到的例子不太同樣,小編其實不肯定是否是享元模式,請你們多多指教

首先定義一個工具類 HashUtil,計算內容的hash值(注:計算hash是從 www.cnblogs.com/oxgen/p/396… 處複製的)

public class HashUtil {
    public static String computeHashId(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        // http://stackoverflow.com/questions/332079
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}
複製代碼

資源類 Resource,至關於享元類的內部狀態

public class Resource {
    private String hashId;
    private int byteSize;
    private String content;

    public Resource(String content) {
        this.content = content;
        this.hashId = HashUtil.computeHashId(content);   // 文件的hash值
        this.byteSize = content.length();
    }
    // ....getter、setter、toString...
}
複製代碼

用戶的文件類 File,其中的 resource 爲內部狀態,owner和filename爲外部狀態

public  class File {
    protected String owner;
    protected String filename;
    protected Resource resource;

    public File(String owner, String filename) {
        this.owner = owner;
        this.filename = filename;
    }

    public String fileMeta() {// 文件存儲到文件系統中須要的key
        if (this.owner == null || filename == null || resource == null) {
            return "未知文件";
        }
        return owner + "-" + filename + resource.getHashId();
    }


    public String display() {
        return fileMeta() + ", 資源內容:" + getResource().toString();
    }
    // ....getter、setter、toString...
}
複製代碼

網盤類 PanServer,該類使用單例模式(在其餘例子中該類還使用工廠方法模式),在upload方法中根據所上傳的文件的hashId判斷是否已經有相同內容的文件存在,存在則引用,不存在才上傳該文件

public class PanServer {
    private static PanServer panServer = new PanServer(); // 單例模式
    private Map<String, Resource> resourceSystem; // 資源系統,至關於享元池
    private Map<String, File> fileSystem;   // 文件系統

    public PanServer() {
        resourceSystem = new HashMap<String, Resource>();
        fileSystem = new HashMap<String, File>();
    }

    public static PanServer getInstance() {
        return panServer;
    }

    public String upload(String username, LocalFile localFile) {
        long startTime = System.currentTimeMillis();
        File file = new File(username, localFile.getFilename());
        String hashId = HashUtil.computeHashId(localFile.getContent());     // 計算文件hash值
        System.out.println(username + " 上傳文件");
        try {
            if (resourceSystem.containsKey(hashId)) {
                System.out.println(String.format("檢測到內容相同的文件《%s》,爲了節約空間,重用文件", localFile.getFilename()));
                file.setResource(this.resourceSystem.get(hashId));
                Thread.sleep(100);
            } else {
                System.out.println(String.format("文件《%s》上傳中....", localFile.getFilename()));
                Resource newResource = new Resource(localFile.getContent());
                file.setResource(newResource);
                this.resourceSystem.put(newResource.getHashId(), newResource); // 將資源對象存儲到資源池中
                Thread.sleep(3000);     // 上傳文件須要耗費必定時間
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        fileSystem.put(file.fileMeta(), file);
        long endTime = System.currentTimeMillis();
        System.out.println(String.format("文件上傳完成,共耗費 %s 毫秒\n", endTime - startTime));
        return file.fileMeta();
    }


    public void download(String fileKey) {
        File file = this.fileSystem.get(fileKey);
        if (file == null) {
            System.out.println("文件不存在");
        } else {
            System.out.println("下載文件:" + file.display());
        }
        // 轉爲 LocalFile 返回
    }
}
複製代碼

客戶端和本地文件類

public class LocalFile {
    private String filename;
    private String content;

    public LocalFile(String filename, String content) {
        this.filename = filename;
        this.content = content;
    }
    //...省略...
}

public class Test {
    public static void main(String[] args) {
        PanServer panServer = PanServer.getInstance();

        String fileContent = "這是一個pdf文件《設計模式:從入門到放棄》";
        LocalFile localFile1 = new LocalFile("小明的設計模式.pdf", fileContent);
        String fikeKey1 = panServer.upload("小明", localFile1);

        LocalFile localFile2 = new LocalFile("大明的設計模式.pdf", fileContent);
        String fikeKey2 = panServer.upload("大明", localFile2);

        panServer.download(fikeKey1);
        panServer.download(fikeKey2);
    }
}
複製代碼

輸出

小明 上傳文件
文件《小明的設計模式.pdf》上傳中....
文件上傳完成,共耗費 3077 毫秒

大明 上傳文件
檢測到內容相同的文件《大明的設計模式.pdf》,爲了節約空間,重用文件
文件上傳完成,共耗費 100 毫秒

下載文件:小明-小明的設計模式.pdf-f73ea50f00f87b42d1f2e4eb6b71d383, 資源內容:Resource {hashId='f73ea50f00f87b42d1f2e4eb6b71d383', byteSize=22, content='這是一個pdf文件《設計模式:從入門到放棄》'}
下載文件:大明-大明的設計模式.pdf-f73ea50f00f87b42d1f2e4eb6b71d383, 資源內容:Resource {hashId='f73ea50f00f87b42d1f2e4eb6b71d383', byteSize=22, content='這是一個pdf文件《設計模式:從入門到放棄》'}
複製代碼

小明和大明各自上傳了一份文件,文件的內容(內部狀態)是相同的,可是名稱(外部狀態)不一樣,因爲內部狀態相同沒有必要重複存儲,因此內部狀態之拷貝了一份

享元模式總結

享元模式的主要優勢以下:

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

享元模式的主要缺點以下:

  • 享元模式使得系統變得複雜,須要分離出內部狀態和外部狀態,這使得程序的邏輯複雜化。
  • 爲了使對象能夠共享,享元模式須要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間變長。

適用場景

  • 一個系統有大量相同或者類似的對象,形成內存的大量耗費。
  • 對象的大部分狀態均可之外部化,能夠將這些外部狀態傳入對象中。
  • 在使用享元模式時須要維護一個存儲享元對象的享元池,而這須要耗費必定的系統資源,所以,應當在須要屢次重複使用享元對象時才值得使用享元模式。

源碼分析享元模式的典型應用

String中的享元模式

Java中將String類定義爲final(不可改變的),JVM中字符串通常保存在字符串常量池中,java會確保一個字符串在常量池中只有一個拷貝,這個字符串常量池在JDK6.0之前是位於常量池中,位於永久代,而在JDK7.0中,JVM將其從永久代拿出來放置於堆中。

咱們作一個測試:

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        String s3 = "he" + "llo";
        String s4 = "hel" + new String("lo");
        String s5 = new String("hello");
        String s6 = s5.intern();
        String s7 = "h";
        String s8 = "ello";
        String s9 = s7 + s8;
        System.out.println(s1==s2);//true
        System.out.println(s1==s3);//true
        System.out.println(s1==s4);//false
        System.out.println(s1==s9);//false
        System.out.println(s4==s5);//false
        System.out.println(s1==s6);//true
    }
}
複製代碼

String類的final修飾的,以字面量的形式建立String變量時,jvm會在編譯期間就把該字面量hello放到字符串常量池中,由Java程序啓動的時候就已經加載到內存中了。這個字符串常量池的特色就是有且只有一份相同的字面量,若是有其它相同的字面量,jvm則返回這個字面量的引用,若是沒有相同的字面量,則在字符串常量池建立這個字面量並返回它的引用。

因爲s2指向的字面量hello在常量池中已經存在了(s1先於s2),因而jvm就返回這個字面量綁定的引用,因此s1==s2

s3中字面量的拼接其實就是hello,jvm在編譯期間就已經對它進行優化,因此s1和s3也是相等的。

s4中的new String("lo")生成了兩個對象,lonew String("lo")lo存在字符串常量池,new String("lo")存在堆中,String s4 = "hel" + new String("lo")實質上是兩個對象的相加,編譯器不會進行優化,相加的結果存在堆中,而s1存在字符串常量池中,固然不相等。s1==s9的原理同樣。

s4==s5兩個相加的結果都在堆中,不用說,確定不相等。

s1==s6中,s5.intern()方法能使一個位於堆中的字符串在運行期間動態地加入到字符串常量池中(字符串常量池的內容是程序啓動的時候就已經加載好了),若是字符串常量池中有該對象對應的字面量,則返回該字面量在字符串常量池中的引用,不然,建立複製一份該字面量到字符串常量池並返回它的引用。所以s1==s6輸出true。

Integer 中的享元模式

使用例子以下:

public static void main(String[] args) {
        Integer i1 = 12 ;
        Integer i2 = 12 ;
        System.out.println(i1 == i2);

        Integer b1 = 128 ;
        Integer b2 = 128 ;
        System.out.println(b1 == b2);
    }
複製代碼

輸出是

true
false
複製代碼

爲何第一個是true,第二個是false? 反編譯後能夠發現 Integer b1 = 128; 實際變成了 Integer b1 = Integer.valueOf(128);,因此咱們來看 Integer 中的 valueOf 方法的實現

public final class Integer extends Number implements Comparable<Integer> {
    public static Integer valueOf(int var0) {
        return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
    }
    //...省略...
}
複製代碼

IntegerCache 緩存類

//是Integer內部的私有靜態類,裏面的cache[]就是jdk事先緩存的Integer。
    private static class IntegerCache {
        static final int low = -128;//區間的最低值
        static final int high;//區間的最高值,後面默認賦值爲127,也能夠用戶手動設置虛擬機參數
        static final Integer cache[]; //緩存數組

        static {
            // high value may be configured by property
            int h = 127;
            //這裏能夠在運行時設置虛擬機參數來肯定h  :-Djava.lang.Integer.IntegerCache.high=250
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {//用戶設置了
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);//雖然設置了可是仍是不能小於127
                // 也不能超過最大值
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            //循環將區間的數賦值給cache[]數組
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
    }
複製代碼

能夠看到 Integer 默認先建立並緩存 -128 ~ 127 之間數的 Integer 對象,當調用 valueOf 時若是參數在 -128 ~ 127 之間則計算下標並從緩存中返回,不然建立一個新的 Integer 對象

Long中的享元模式

public final class Long extends Number implements Comparable<Long> {
    public static Long valueOf(long var0) {
        return var0 >= -128L && var0 <= 127L ? Long.LongCache.cache[(int)var0 + 128] : new Long(var0);
    }   
    private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }
    //...
}
複製代碼

同理,Long 中也有緩存,不過不能指定緩存最大值

Apache Commons Pool2中的享元模式

對象池化的基本思路是:將用過的對象保存起來,等下一次須要這種對象的時候,再拿出來重複使用,從而在必定程度上減小頻繁建立對象所形成的開銷。用於充當保存對象的「容器」的對象,被稱爲「對象池」(Object Pool,或簡稱Pool)

Apache Commons Pool實現了對象池的功能。定義了對象的生成、銷燬、激活、鈍化等操做及其狀態轉換,並提供幾個默認的對象池實現。

有幾個重要的對象:

PooledObject(池對象):用於封裝對象(如:線程、數據庫鏈接、TCP鏈接),將其包裹成可被池管理的對象。
PooledObjectFactory(池對象工廠):定義了操做PooledObject實例生命週期的一些方法,PooledObjectFactory必須實現線程安全。
Object Pool (對象池):Object Pool負責管理PooledObject,如:借出對象,返回對象,校驗對象,有多少激活對象,有多少空閒對象。

// 對象池
 private final Map<S, PooledObject<S>> allObjects = new ConcurrentHashMap<S, PooledObject<S>>();
複製代碼

重要方法:

borrowObject:從池中借出一個對象。
returnObject:將一個對象返還給池。

因爲篇幅較長,後面會專門出一篇介紹並使用 Apache Commons Pool2 的文章,敬請期待

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+內存分析
Java中String字符串常量池
Integer的享元模式解析
7種結構型模式之:享元模式(Flyweight)與數據庫鏈接池的原理
Apache commons-pool2-2.4.2源碼學習筆記
Apache Commons Pool2 源碼分析

相關文章
相關標籤/搜索