详解享元模式

引言

在计算机中,内存是非常宝贵的资源,而程序中可能会有大量相似或相同的对象,它们的存在浪费了许多空间。而享元模式通过共享这些对象,从而解决这种问题的。

1.概念

享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State):

(1)内部状态:存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。如字符不会随外部环境的变化而变化,无论在任何环境下字符"a"始终是"a",都不会变成"b"。

(2)外部状态:随环境改变而改变的、不可以共享的状态。外部状态通常由客户端保存,并在享元对象被创建之后,需要用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色和字体大小,字符"a"可以是红色的,也可以是绿色的,可以是小四号字体,也可以是三号字体。

区分内部状态和外部状态保证可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。

2.模式结构

3.模式分析

Flyweight:抽象享元类,可以是接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。

ConcreteFlyweight:具体享元类,它实现了抽象享元类,其实例称为享元对象。在具体享元类中为内部状态提供了存储空间,通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。

UnsharedConcreteFlyweight:非共享具体享元类,并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当需要一个非共享具体享元类的对象时可以直接通过实例化创建。

享元类的设计很重要,核心代码如下:

java 复制代码
public class Flyweight {

    //内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的

    private String intrinsicState;

    public Flyweight(String intrinsicState) {

        this.intrinsicState = intrinsicState;

    }

    //外部状态extrinsicState在使用时由外部设置,不保存在享元对象中

    public void operation(String extrinsicState){

        //设置外部状态

    }

}

FlyweightFactory:享元工厂类,享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储"键值对"的集合(或其他类型的集合),可以结合工厂模式进行设计。当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。核心代码如下:

java 复制代码
public class FlyweightFactory {

    //定义一个HashMap用于存储享元对象,实现享元池

    private HashMap<String,Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String key) {

        //如果对象存在,则直接从享元池获取

        if (flyweights.containsKey(key)) {

            return (Flyweight) flyweights.get(key);

        } else {//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回

            Flyweight fw = new ConcreteFlyweight();

            flyweights.put(key, fw);

            return fw;

        }

    }

}

4.具体实例分析

Shape:抽象享元类,包含享元类的内部状态shape和外部状态xy坐标。具体代码如下:

java 复制代码
//抽象享元类

public abstract class  {

    //内部状态是shape

    public abstract String getShape();

    //外部状态是中心xy坐标

    public void draw(int x,int y){

        System.out.println("形状:" + this.getShape() + " 坐标x:" + x + " 坐标y:" + y);

    }

}

Circle:具体享元类,继承抽象享元类Shape,并实现getShape()方法,返回实例的内部状态:形状是圆。具体代码如下:

java 复制代码
//具体享元类:圆

public class Circle extends Shape{

    public String getShape(){

        return "圆";

    }

}

Rectangle:具体享元类,继承抽象享元类Shape,并实现getShape()方法,返回实例的内部状态:形状是矩形。具体代码如下:

java 复制代码
//具体享元类:矩形

public class Rectangle extends Shape{

    public String getShape(){

        return "矩形";

    }

}

ShapeFlyweightFactory:享元工厂类,使用单例模式设计享元工厂类,保证全局只有唯一的工厂实例(懒汉模式实现)。同时创建HashMap作为共享享元类的享元池,保证不重复创建相同或相似的对象。具体代码如下:

java 复制代码
//享元工厂类:单例模式

public class ShapeFlyweightFactory {

    //单例模式实现只有一个全局唯一的工厂(懒汉模式)

    private static ShapeFlyweightFactory shapeFlyweightFactory = new ShapeFlyweightFactory();

    //定义一个HashMap用于存储享元对象,实现享元池

    private static HashMap<String,Shape> flyweights = new HashMap<>();

    //返回唯一实例工厂

    public static ShapeFlyweightFactory getInstance(){

        return shapeFlyweightFactory;

    }

    public Shape getShapeFlyweight(String shape) {

        //如果对象存在,则直接从享元池获取

        if (flyweights.containsKey(shape)) {

            return flyweights.get(shape);

        } else {//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回

            Shape flyweight;

            if(shape.equals("圆")){

                flyweight = new Circle();

            }else if(shape.equals("矩形")) {

                flyweight = new Rectangle();

            }else{

                System.out.println("系统没有这个形状!");

                return null;

            }

            flyweights.put(shape, flyweight);

            return flyweight;

        }

    }

}

Client:客户端,创建多个相同对象,观察是否是同一个对象,并给对象添加外部状态。具体代码如下:

java 复制代码
public class Client {

    public static void main(String[] args) {

        //获得唯一的享元工厂实例

        ShapeFlyweightFactory shapeFlyweightFactory = ShapeFlyweightFactory.getInstance();

        //从工厂中获得形状

        Shape circle1 = shapeFlyweightFactory.getShapeFlyweight("圆");

        Shape circle2 = shapeFlyweightFactory.getShapeFlyweight("圆");

        System.out.println("circle1==circle2? " + (circle1 == circle2));

        Shape rectangle1 = shapeFlyweightFactory.getShapeFlyweight("矩形");

        Shape rectangle2 = shapeFlyweightFactory.getShapeFlyweight("矩形");

        System.out.println("rectangle1==rectangle2? " + (rectangle1 == rectangle2));

        //为形状添加外部状态:颜色

        circle1.draw(1,2);

        circle2.draw(2,3);

        rectangle1.draw(1,3);

        rectangle2.draw(4,6);

    }

}

运行代码,结果如下:

结果显示,创建多个相同对象,享元模式的背后只会创建一个,并存储在享元池中共享,这样就节省了内存资源。

5.模式扩展

(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。

(2)在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。

(3)去掉非共享具体享元类,标准的享元模式就演变为单纯享元模式,即单纯享元模式中所有具体享元类都可以共享。

(4)享元模式+组合模式=复合享元模式,统一对多个享元对象设置外部状态。

通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。

(5)Java中String类采用享元模式。

java 复制代码
public class StringFlyweight {

    public static void main(String[] args) {

        String str1 = "abcd";

        String str2 = "abcd";

        String str3 = "ab" + "cd";

        String str4 = "ab";

        str4 += "cd";

        System.out.println(str1 == str2);

        System.out.println(str1 == str3);

        System.out.println(str1 == str4);

        str2 += "e";

        System.out.println(str1 == str2);

    }

}

这段代码的输出如下:

在Java中,如果每次执行String str1 = "abcd"创建一个新的字符串对象,内存开销将会很大。为此,Java设计了字符串常量池,字符串常量池中存储着唯一的字符串对象。每次创建相同的字符串都是对这唯一对象的引用,因此str1==str2==str3成立。而str4创建时由于初始值是"ab",进行字符串连接操作Java为其重新分配了内存,创建了一个新的对象"abcd",因此str4!=str1。而str2再次进行连接操作时,也创建了一个新的对象"abcde",因此str2此时和str1指向的不是同一个对象。

关于Java String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为"Copy On Write"。

6.优缺点

主要优点如下:

(1)可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。

(2)享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

主要缺点如下:

(1)享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。

(2)为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。

7.适用情况

(1)一个系统有大量相同或者相似的对象,造成内存的大量耗费。

(2)对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。

(3)在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

相关推荐
qq_316837752 分钟前
uniapp 打包apk 播放带透明通道的webm格式视频
java·前端·uni-app
编织幻境的妖3 分钟前
关于阿里云 dataworks 运维中心下的任务运维的问题
java·运维·阿里云
Kali_077 分钟前
StarSpider 星蛛 爬虫 Java框架 可以实现 lazy爬取 实现 HTML 文件的编译,子标签缓存等操作
java·爬虫·html
青云交12 分钟前
Java 大视界 -- 深度洞察 Java 大数据安全多方计算的前沿趋势与应用革新(52)
java·大数据·密码学·安全多方计算·分布式计算·医疗数据·技术融合
雨 子1 小时前
Idea ⽆ Maven 选项
java·maven·intellij-idea
lihan_freak1 小时前
java中的抽象类和接口
java·开发语言·接口·抽象类
P7进阶路1 小时前
【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列
java·rabbitmq·java-rabbitmq
sjsjsbbsbsn1 小时前
Java Web 开发中的分页与参数校验
java·spring boot·spring·状态模式·hibernate
lihan_freak2 小时前
java中equals和hashCode为什么要一起重写
java·面试·哈希算法·equals·hashcode
蝴蝶不愿意2 小时前
Java基础学习笔记-static关键字
java·开发语言·学习