享元模式


定义

这节中我们将介绍一种新的结构型模式——享元模式 (Flyweight Pattern),Flyweight 模式是以共享的模式来支持大量细粒度对象的复用。听起来可能有点绕,Java 中的 String 就是享元模式的一个应用:

String a = "abc";
String b = "abc";
System.out.println(a == b); // true

上述例子中,分别创建两个字符串对象的时候,a、b 其实都指向了常量池中的某个字符串“abc”,这种对象创建的模式,就避免了大量对象创建时非必要的资源消耗,享元模式的“享”就有一物被众人所共享的意思,所以享元模式也是池技术的重要实现方式,其定义如下:使用共享对象有效地支持大量的细粒度的对象。

使用实例

下面我们用一个最简单的考试报名的例子进行说明,假设我们有 2 个科目,有 3 位考生分别进行报考,我们一般会定义考试实体 ExamInfo,如果不使用模式的话,可以想象,每次有考生参与一场科目考试的话,我们就会实例化一个 ExamInfo,总共我们要实例化 6 个这样的实体,倘若使用享元模式,我们就只需要实例化 2 个这样的实体,然后通过内部状态的 set 方法进行不同对象的赋值操作,节省了不少的内存,很神奇吧?

先来看看我们的 ExamInfo 如何?

public class ExamInfo {

    // 内部状态,用于在各个对象之间共享,不随环境改变而改变,存储在享元对象内部,往往作为对象的动态附加信息存在
    private String user; // 考生

    // 外部状态,随环境改变而改变,属于不可共享的状态,是对象得以依赖的一个标记
    private String subject; // 考试科目

    public ExamInfo(String subject) {
        this.subject = subject;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    @Override
    public String toString() {
        return "ExamInfo{" +
                "user='" + user + '\'' +
                ", subject='" + subject + '\'' +
                '}';
    }
}

在考试信息里面,我们将考试科目作为考试信息的唯一标识,以此来实现对象复用。

然后是我们的享元工厂,也就是我们的池,该池中留有细粒度对象的引用实例,具体代码如下:

public class ExamInfoFactory {

    // 对象池,用来支持细粒度对象的复用
    private static HashMap<String, ExamInfo> pool = new HashMap<>();

    public static ExamInfo getExamInfo(String subject) {
        // 设置返回的对象
        ExamInfo examInfo = null;
        if (!pool.containsKey(subject)) {
            System.out.println("建立对象,并放到池中..." + subject);
            examInfo = new ExamInfo(subject);
            pool.put(subject, examInfo);
        } else {
            examInfo = pool.get(subject);
            System.out.println("直接从池中获取..." + subject);
        }
        return examInfo;
    }
}

这里我们使用一个 map 对象来实现池的功能,通过科目我们可以获取池中的某个考试信息实例,如果存在则直接从池中获取返回,如果不存在,我们会手动 new 一个考试信息,然后再将其放入池中,之后再返回。

接下来是我们的测试类,也就是 main 函数:

public class Main {

    public static void main(String[] args) {

        // 假设有2个科目,初始化一遍池子
        for (int i = 0; i < 2; i++) {
            String subject = "科目" + i;
            ExamInfoFactory.getExamInfo(subject);
        }
        // 假设3个考生考试
        for(int i = 0; i < 3; i++) {
            for (int j = 0; j < 2; j++) {
                ExamInfo examInfo = ExamInfoFactory.getExamInfo("科目" + j);
                examInfo.setUser("考生" + i);
                System.out.println(examInfo);
            }
        }
    }
}

测试输出如下:

建立对象,并放到池中...科目0
建立对象,并放到池中...科目1
直接从池中获取...科目0
ExamInfo{user='考生0', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生0', subject='科目1'}
直接从池中获取...科目0
ExamInfo{user='考生1', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生1', subject='科目1'}
直接从池中获取...科目0
ExamInfo{user='考生2', subject='科目0'}
直接从池中获取...科目1
ExamInfo{user='考生2', subject='科目1'}

从上面的信息中我们可以发现,实际上初始化 ExamInfo 实体的操作我们只进行了 2 次,也就说内存中保存的对象引用只有 2 份,相比预不使用享元模式需要进行 6 次实例化的方案是不是一下省了部分内存,设想一下,倘若有 100000 个考生那又节省了多少内存?

组成角色

享元模式包含的角色列举如下:

  • 抽象享元角色(Flyweight):一般是一个具体的抽象类,同时定义了对象的外部状态和内部状态的接口或实现;
  • 具体享元角色(ConcreteFlyweight):具体的一个产品类,实现了抽象享元角色中定义的接口,该角色需要注意的是内部状态的处理应该与环境无关;
  • 享元工厂(FlyweightFactory):该角色指责一般比较清晰,就是一个池工厂,提供池对象和获取池中对象的方法

享元模式代码实现

享元模式的几个角色上面已经介绍了,该角色对应的通用代码如下,首先是抽象享元角色:

public abstract class Flyweight {

    // 内部状态
    private String intrinsic;

    // 外部状态
    protected final String extrinsic;

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

    // 定义的业务操作
    public abstract void operate();

    // 内部状态的getter、setter
    public String getIntrinsic() {
        return intrinsic;
    }

    public void setIntrinsic(String intrinsic) {
        this.intrinsic = intrinsic;
    }
}

接下来是具体的享元角色:

public class ConcreteFlyweight extends Flyweight{

    // 接受外部状态
    public ConcreteFlyweight(String extrinsic) {
        super(extrinsic);
    }

    // 根据外部状态进行一些逻辑处理
    @Override
    public void operate() {
        // 具体的业务逻辑
    }
}

最后是享元工厂:

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 = new ConcreteFlyweight(extrinsic);
            // 再放入池中
            pool.put(extrinsic, flyweight);
        } else {
            // 池中有对象,则直接返回
            flyweight = pool.get(extrinsic);
        }
        return flyweight;
    }
}

总结

本节中,我们讲了下什么是享元模式,以及为什么要使用享元模式,下面我们归纳下本节的知识点,包括享元模式的应用场景,总结下如下:


Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
 Previous
代理模式 代理模式
定义代理模式的定义其实比较简单:代理模式给某对象提供一个代理对象,由代理对象来控制对原对象的引用。生活中比较常见的代理模式的应用比如:火车票代购、代办保险、UU 跑腿、武侠片中的替身、nginx 反向代理等等这种类似于中介的模式统统可以归于
2022-03-09
Next 
装饰器模式 装饰器模式
定义本节我们要学习的设计模式叫做——装饰器模式,何为装饰器模式?
2022-03-09
  TOC