享元模式

享元模式

案例

張三和李四剛剛考完期中考試的語文和數(shù)學(xué),但不是很理想。老師在課堂的講的感覺還不是很懂,所以想找老師要答案仔細(xì)看看解題過程。接下來就用程序來模擬這一過程,假設(shè)考了語文和數(shù)學(xué)兩個(gè)科目。

1.首先定義兩個(gè)試卷類:

語文試卷類:

/**
 * 語文試卷答案類
 */
public class ChineseTestAnswer {
    // 試卷答案內(nèi)容
    private String answer;

    public ChineseTestAnswer() {
        System.out.println("老師從試卷庫中取出語文試卷答案帶進(jìn)教室");
        this.answer = "答案:A、B、C...";
    }

    public String showAnswer() {
        return this.answer;
    }
}

數(shù)學(xué)試卷類:

/**
 * 數(shù)學(xué)書卷答案類
 */
public class MathTestAnswer {
    // 試卷答案內(nèi)容
    private String answer;

    public MathTestAnswer() {
        System.out.println("老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室");
        this.answer = "答案:C、B、A...";;
    }

    public String showAnswer() {
        return this.answer;
    }
}

2.然后是學(xué)生類:

/**
 * 學(xué)生類
 */
public class Student {
    private String name;
    private ChineseTestAnswer chineseTestAnswer;
    private MathTestAnswer mathTestAnswer;

    public Student(String name) {
        this.name = name;
    }

    public void showChineseAnswer() {
        System.out.println("學(xué)生" + this.name + "查看語文試卷答案[" + chineseTestAnswer.showAnswer() + "]");
    }

    public void showMathTestAnswer() {
        System.out.println("學(xué)生" + this.name + "查看語文試卷答案[" + mathTestAnswer.showAnswer() + "]");
        mathTestAnswer.showAnswer();
    }

    public void setChineseTestAnswer(ChineseTestAnswer chineseTestAnswer) {
        System.out.println("老師把語文試卷答案發(fā)給學(xué)生:" + this.name);
        this.chineseTestAnswer = chineseTestAnswer;
    }

    public void setMathTestAnswer(MathTestAnswer mathTestAnswer) {
        System.out.println("老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:" + this.name);
        this.mathTestAnswer = mathTestAnswer;
    }
}

3.測(cè)試類:

public class Main {
    public static void main(String[] args) {
        Student zs = new Student("張三");
        zs.setChineseTestAnswer(new ChineseTestAnswer());
        zs.showChineseAnswer();
        zs.setMathTestAnswer(new MathTestAnswer());
        zs.showMathTestAnswer();
        System.out.println("-----------------------------------------");
        Student ls = new Student("李四");
        ls.setChineseTestAnswer(new ChineseTestAnswer());
        ls.showChineseAnswer();
        ls.setMathTestAnswer(new MathTestAnswer());
        ls.showMathTestAnswer();
    }
}

4.測(cè)試結(jié)果:

老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師把語文試卷答案發(fā)給學(xué)生:張三
學(xué)生張三查看語文試卷答案[答案:A、B、C...]
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:張三
學(xué)生張三查看語文試卷答案[答案:C、B、A...]
-----------------------------------------
老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師把語文試卷答案發(fā)給學(xué)生:李四
學(xué)生李四查看語文試卷答案[答案:A、B、C...]
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
老師把數(shù)學(xué)試卷答案發(fā)給學(xué)生:李四
學(xué)生李四查看語文試卷答案[答案:C、B、A...]

從這一過程可以看到,老師給每個(gè)人的每個(gè)科目都拿了考試答案,這樣張三和李四就可以愉快的參考答案仔細(xì)解決自身的問題所在了。但是作為一個(gè)以節(jié)約為榮,一浪費(fèi)為恥的張三覺得,老師給每個(gè)人的各個(gè)科目都發(fā)答案浪費(fèi)了紙張。他覺得只用給每個(gè)科目一份答案就好了,一個(gè)人再看語文答案的時(shí)候,另一個(gè)人可以看數(shù)學(xué)答案。這樣試卷的答案就只用有一份就可以了,而且其他同學(xué)想看,也不用在浪費(fèi)紙張了,所有的人都用那一份語文答案和一份數(shù)學(xué)答案。張三這一思想恰好與設(shè)計(jì)模式中的享元模式相似,下面就來介紹以下享元模式。

模式介紹

享元模式(英語:Flyweight Pattern)是一種結(jié)構(gòu)型設(shè)計(jì)模式。它使用共享物件,用來盡可能減少內(nèi)存使用量以及分享資訊給盡可能多的相似物件;它適合用于只是因重復(fù)而導(dǎo)致使用無法令人接受的大量內(nèi)存的大量物件。通常物件中的部分狀態(tài)是可以分享。常見做法是把它們放在外部數(shù)據(jù)結(jié)構(gòu),當(dāng)需要使用時(shí)再將它們傳遞給享元。

角色構(gòu)成

  • Flyweight(抽象享元類):通常是一個(gè)接口或抽象類,在抽象享元類中聲明了具體享元類公共的方法,這些方法可以向外界提供享元對(duì)象的內(nèi)部數(shù)據(jù)(內(nèi)部狀態(tài)),同時(shí)也可以通過這些方法來設(shè)置外部數(shù)據(jù)(外部狀態(tài))。
  • ConcreteFlyweight(具體享元類):它實(shí)現(xiàn)了抽象享元類,其實(shí)例稱為享元對(duì)象;在具體享元類中為內(nèi)部狀態(tài)提供了存儲(chǔ)空間。通常我們可以結(jié)合單例模式來設(shè)計(jì)具體享元類,為每一個(gè)具體享元類提供唯一的享元對(duì)象。
  • UnsharedConcreteFlyweight(非共享具體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設(shè)計(jì)為非共享具體享元類;當(dāng)需要一個(gè)非共享具體享元類的對(duì)象時(shí)可以直接通過實(shí)例化創(chuàng)建。
  • FlyweightFactory(享元工廠類):享元工廠類用于創(chuàng)建并管理享元對(duì)象,它針對(duì)抽象享元類編程,將各種類型的具體享元對(duì)象存儲(chǔ)在一個(gè)享元池中,享元池一般設(shè)計(jì)為一個(gè)存儲(chǔ)“鍵值對(duì)”的集合(也可以是其他類型的集合),可以結(jié)合工廠模式進(jìn)行設(shè)計(jì);當(dāng)用戶請(qǐng)求一個(gè)具體享元對(duì)象時(shí),享元工廠提供一個(gè)存儲(chǔ)在享元池中已創(chuàng)建的實(shí)例或者創(chuàng)建一個(gè)新的實(shí)例(如果不存在的話),返回新創(chuàng)建的實(shí)例并將其存儲(chǔ)在享元池中。

在享元模式中引入了一個(gè)工廠類,它的作用在于提供一個(gè)用于存儲(chǔ)享元對(duì)象的享元池,當(dāng)用戶需要對(duì)象時(shí),首先從享元池中獲取,如果享元池中不存在,則創(chuàng)建一個(gè)新的享元對(duì)象返回給用戶,并在享元池中保存該新增對(duì)象。

UML類圖

flyweight

享元類的設(shè)計(jì)是享元模式的要點(diǎn)之一,在享元類中要將內(nèi)部狀態(tài)外部狀態(tài)分開處理,通常將內(nèi)部狀態(tài)作為享元類的成員變量,而外部狀態(tài)通過注入的方式添加到享元類中。

代碼改造

根據(jù)上面的介紹,依照我們的案例,可以分析出語文答案和數(shù)學(xué)答案就是具體的享元類,而試卷中的答案內(nèi)容就可以看作是內(nèi)部狀態(tài),而學(xué)生就可以看作是外部狀態(tài)。下面就是改造后的內(nèi)容。

1.首先是抽象享元類:

/**
 * 抽象享元類
 */
public abstract class TestAnswer {
    // 試卷答案內(nèi)容
    private String answer;

    public abstract void showAnswer(Student student);

    public String getAnswer() {
        return answer;
    }

    public void setAnswer(String answer) {
        this.answer = answer;
    }
}

2.兩個(gè)具體享元類:

語文試卷答案類:

/**
 * 具體享元類
 */
public class ChineseTestAnswer extends TestAnswer {
    public ChineseTestAnswer() {
        System.out.println("老師從試卷庫中取出語文試卷答案帶進(jìn)教室");
        this.setAnswer("答案:A、B、C...");
    }

    @Override
    public void showAnswer(Student student) {
        System.out.println("學(xué)生" + student.getName() + "查看語文試卷答案[" + this.getAnswer() + "]");
    }
}

數(shù)學(xué)試卷類:

/**
 * 具體享元類
 */
public class MathTestAnswer extends TestAnswer {
    public MathTestAnswer() {
        System.out.println("老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室");
        this.setAnswer("答案:C、B、A...");
    }

    @Override
    public void showAnswer(Student student) {
        System.out.println("學(xué)生" + student.getName() + "查看數(shù)學(xué)試卷答案[" + this.getAnswer() + "]");
    }
}

3.外部狀態(tài)類:

/**
 * 外部狀態(tài):學(xué)生類
 */
public class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

4.享元工廠類:

/**
 * 享元工廠類
 */
public class TestAnswerFactory {
    private static Map<String, TestAnswer> map = new HashMap<>();

    public static TestAnswer getTestAnswer(String type) {
        TestAnswer testAnswer = map.get(type);
        if(testAnswer == null){
            if(type.equals("chinese")){
                testAnswer = new ChineseTestAnswer();
            }else if (type.equals("math")){
                testAnswer = new MathTestAnswer();
            }else {
                throw new IllegalArgumentException("輸入的試卷類型不存在");
            }
        }
        map.put(type, testAnswer);
        return testAnswer;
    }
}

5.測(cè)試類:

public class Main {
    public static void main(String[] args) {
        TestAnswer chinese = TestAnswerFactory.getTestAnswer("chinese");
        TestAnswer math = TestAnswerFactory.getTestAnswer("math");
        Student zs = new Student("張三");
        chinese.showAnswer(zs);
        math.showAnswer(zs);
        System.out.println("-----------------------------------------");
        Student ls = new Student("李四");
        chinese.showAnswer(ls);
        math.showAnswer(ls);
    }
}

6.測(cè)試結(jié)果:

老師從試卷庫中取出語文試卷答案帶進(jìn)教室
老師從試卷庫中取出數(shù)學(xué)試卷答案帶進(jìn)教室
學(xué)生張三查看語文試卷答案[答案:A、B、C...]
學(xué)生張三查看數(shù)學(xué)試卷答案[答案:C、B、A...]
-----------------------------------------
學(xué)生李四查看語文試卷答案[答案:A、B、C...]
學(xué)生李四查看數(shù)學(xué)試卷答案[答案:C、B、A...]

可以看到經(jīng)過改造后,語文和數(shù)學(xué)兩個(gè)科目只用拿一份答案,張三和李四也能參照答案,同時(shí)節(jié)約了紙張。

這里的的試卷工廠就像是一個(gè)池子一樣,它里面存放了我們需要的試卷答案,同時(shí)這些答案是共享的。我們的案例中不存在非共享具體享元類,所以也可以說是單純享元模式。還有一種稱之為復(fù)合享元模式,它是指將一些單純享元對(duì)象使用組合模式加以組合,形成復(fù)合享元對(duì)象,這樣的復(fù)合享元對(duì)象本身不能共享,但是它們可以分解成單純享元對(duì)象,而后者則可以共享。

模式應(yīng)用

享元模式在 JDK 中的一些包裝類中應(yīng)用非常廣泛,包括:Integer.valueof(int) 、Boolean.valueof(boolean)Byte.valueof(byte)、Character.valueof(char)、Long.valueof(long),下面就看一下常用的Integer類:

1.測(cè)試代碼:

public class Main {
    public static void main(String[] args) {
        Integer integer1 = Integer.valueOf(127);
        Integer integer2 = Integer.valueOf(127);
        System.out.println(integer1 == integer2);

        Integer integer3 = Integer.valueOf(128);
        Integer integer4 = Integer.valueOf(128);
        System.out.println(integer3 == integer4);
    }
}

2.測(cè)試結(jié)果:

true
false

首先我們知道在 Java 中==判斷引用類型數(shù)據(jù)時(shí)比較的是引用地址值,而看到測(cè)試結(jié)果,不知道會(huì)不會(huì)對(duì)于==判斷的到底是什么感到懷疑?首先==對(duì)于引用數(shù)據(jù)類型確實(shí)是引用地址值,所以第二個(gè)判斷結(jié)果為false,而第一個(gè)判斷結(jié)果為true就來看看源碼內(nèi)部是怎樣的:

首先是Integer.valueof(int)方法:

public static Integer valueOf(int i) {
    // 如果傳入的值在 IntegerCache.low 和 IntegerCache.high 之間
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)]; // 就返回?cái)?shù)組中的值
    return new Integer(i); // 否則返回新的 Integer 對(duì)象
}

可以看到這三行代碼很簡單,如果我們傳入的只在IntegerCache.lowIntegerCache.high之間的話,它就會(huì)返回IntegerCache.cache[i + (-IntegerCache.low)],而cacheIntegerCache類中的一個(gè)靜態(tài)數(shù)組,IntegerCacheInteger

中的私有靜態(tài)內(nèi)部類,其具體代碼如下:

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() {}
}

可以看到在這個(gè)類中,做了判斷,如果integerCacheHighPropValue != null那么high就會(huì)使用設(shè)置的值,如果沒有設(shè)置值的話,緩存的最大值high就是127。而最小值在Integer類中有定義:static final int low = -128;,所以就是說在-128到127之間,使用Integer.valueof(int)返回的都是緩存中的對(duì)象,所以就可以解釋integer1 == integer2的結(jié)果為什么是true了。其中緩存的最大值可以通過-XX:AutoBoxCacheMax=<size>來設(shè)置。

總結(jié)

1.主要優(yōu)點(diǎn)

  • 可以極大減少內(nèi)存中對(duì)象的數(shù)量,使得相同或相似對(duì)象在內(nèi)存中只保存一份,從而可以節(jié)約系統(tǒng)資源,提高系統(tǒng)性能。
  • 享元模式的外部狀態(tài)相對(duì)獨(dú)立,而且不會(huì)影響其內(nèi)部狀態(tài),從而使得享元對(duì)象可以在不同的環(huán)境中被共享。

2.主要缺點(diǎn)

  • 享元模式使得系統(tǒng)變得復(fù)雜,需要分離出內(nèi)部狀態(tài)和外部狀態(tài),這使得程序的邏輯復(fù)雜化。
  • 為了使對(duì)象可以共享,享元模式需要將享元對(duì)象的部分狀態(tài)外部化,而讀取外部狀態(tài)將使得運(yùn)行時(shí)間變長。

3.適用場(chǎng)景

  • 一個(gè)系統(tǒng)有大量相同或者相似的對(duì)象,造成內(nèi)存的大量耗費(fèi)。
  • 對(duì)象的大部分狀態(tài)都可以外部化,可以將這些外部狀態(tài)傳入對(duì)象中。
  • 在使用享元模式時(shí)需要維護(hù)一個(gè)存儲(chǔ)享元對(duì)象的享元池,而這需要耗費(fèi)一定的系統(tǒng)資源,因此,應(yīng)當(dāng)在需要多次重復(fù)使用享元對(duì)象時(shí)才值得使用享元模式。

參考資料

本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/flyweight
轉(zhuǎn)載請(qǐng)說明出處,本篇博客地址:http://www.itdecent.cn/p/1a9bebf6f85a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容