美文网首页
享元模式及其应用

享元模式及其应用

作者: 文景大大 | 来源:发表于2022-06-17 10:26 被阅读0次

一、模式介绍

享元模式(Flyweight)是一种对象池的实现方式,就像我们用过的线程池、数据库连接池等一样,主要是为了避免频繁创建或者销毁相同的对象,使得系统尽可能复用已有对象,从而达到节省系统内存的目的。

该模式一般会包含如下几种角色:

  • 抽象享元

    所有具体享元对象的父类或者接口,其中会定义修改外部状态的方法;

  • 具体享元

    继承抽象享元父类或者实现接口的具体的享元对象,其中包含:

    • 内部状态,用来共享的不变状态;
    • 外部状态,可以通过方法被外部更改的状态;
  • 享元工厂

    管理和创建具体享元或者非享元对象的工厂;

我们举个例子来说明享元模式的应用。

在计算机的文档中,可以输入各式各样的字符,比如字符a,在一个文档中出现的次数可能多达成百上千次,我们完全可以使用享元模式来优化字符对象a的创建,不必每出现一次就创建一个字符对象。

public interface ICharacter {
    /**
     * 定位字符,可以修改外部状态
     * @param line 行
     * @param column 列
     */
    void locateCharacter(Long line, Long column);

    /**
     * 将字符写出来
     */
    void writeCharacter();
}
@Slf4j
public class Character implements ICharacter{
    /**
     * 内部状态,所有的字符全部一致,初始化后无法再改变
     */
    private final String color;
    private final Integer size;
    private final String name;

    /**
     * 外部状态,可以通过writeCharacter方法来改变
     */
    private Long line;
    private Long column;

    public Character(String color, Integer size, String name){
        this.color = color;
        this.size = size;
        this.name = name;
    }

    @Override
    public void locateCharacter(Long line, Long column) {
        this.line = line;
        this.column = column;
    }

    @Override
    public void writeCharacter() {
        log.info("在文件的第{}行,第{}列,输出字符{},它的颜色是{},大小是{}", line, column, name, color, size);
    }
}
@Slf4j
public class CharacterFactory {
    /**
     * 保存所有已经创建的字符对象
     */
    private static Map<String, ICharacter> characterMap = new ConcurrentHashMap<>();

    public static ICharacter getCharacter(String name){
        if(characterMap.containsKey(name)){
            log.info("缓存中已经存在字符{}", name);
            return characterMap.get(name);
        }
        log.info("暂无{}这个字符对象,现在创建!", name);
        ICharacter character = new Character("black", 16, name);
        characterMap.put(name, character);
        return character;
    }
}
public class Main {
    public static void main(String[] args) {
        ICharacter a1 = CharacterFactory.getCharacter("a");
        a1.locateCharacter(1L, 3L);
        a1.writeCharacter();

        ICharacter a2 = CharacterFactory.getCharacter("a");
        a2.locateCharacter(2L, 4L);
        a2.writeCharacter();
    }
}
17:11:44.931 [main] INFO 暂无a这个字符对象,现在创建!
17:11:44.937 [main] INFO 在文件的第1行,第3列,输出字符a,它的颜色是black,大小是16
17:11:44.938 [main] INFO 缓存中已经存在字符a
17:11:44.938 [main] INFO 在文件的第2行,第4列,输出字符a,它的颜色是black,大小是16

在这个例子中,只要字符没有被创建过,就会被工厂方法创建,并缓存起来,下次如果还想要这个字符,工厂方法就会直接从缓存中取出先前的字符对象,省去了重新创建多个相同或者相似对象的资源开销。

如上例子中的a1字符和a2字符,我们认为是相似字符对象,它们除了在文档中出现的位置不同之外,其它属性完全相同,甚至如下a3字符和a1字符的位置都相同,那么它们就是相同字符。

ICharacter a3 = CharacterFactory.getCharacter("a");
a3.locateCharacter(1L, 3L);
a3.writeCharacter();

如此,即便文档中有成百上千的a字符,我们内存中只要保留一个该字符对象即可,大大节省了内存空间。

然而,如上的例子中还不是很完美,因为字符a,b,c,d,e......等等,它们都会保持有一个对象,这些对象也是相似对象,排除位置不说,它们也仅仅是name不同而已,color和font大家都是一样的,那为什么还要各自保存相同的属性呢,也会浪费内存空间,所以我们可以更近一步,将内部状态中共性的属性再抽离出来。

@Getter
public class Style {
    /**
     * 内部状态,所有的字符全部一致,初始化后无法再改变
     */
    private final String color;
    private final Integer size;

    public Style(String color, Integer size) {
        this.color = color;
        this.size = size;
    }
}
@Slf4j
public class Character implements ICharacter{
    /**
     * 内部状态,初始化后无法再改变
     */
    private final String name;
    private final Style style;

    /**
     * 外部状态,可以通过writeCharacter方法来改变
     */
    private Long line;
    private Long column;

    public Character(String name, Style style){
        this.name = name;
        this.style = style;
    }

    @Override
    public void locateCharacter(Long line, Long column) {
        this.line = line;
        this.column = column;
    }

    @Override
    public void writeCharacter() {
        log.info("在文件的第{}行,第{}列,输出字符{},它的颜色是{},大小是{}", line, column, name, style.getColor(), style.getSize());
    }
}
@Slf4j
public class CharacterFactory {
    /**
     * 保存所有已经创建的字符对象
     */
    private static Map<String, ICharacter> characterMap = new ConcurrentHashMap<>();

    /**
     * 全局只有一个Style对象
     */
    private static Style characterStyle = new Style("black", 16);

    public static ICharacter getCharacter(String name){
        if(characterMap.containsKey(name)){
            log.info("缓存中已经存在字符{}", name);
            return characterMap.get(name);
        }
        log.info("暂无{}这个字符对象,现在创建!", name);
        ICharacter character = new Character(name, characterStyle);
        characterMap.put(name, character);
        return character;
    }
}

如上例子中,我们把内部状态中共性的属性抽到了一个新的类Style中,并且在工厂方法中初始化一个全局的Style对象,如此所有的字符对象关于color和size属性的值都使用该全局的Style对象,更加节省了内存空间。

二、使用案例

2.1 String中的享元模式应用

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "hello";
    // 对比内存地址来判断是否是一个对象
    // true
    log.info("s1==s2?{}", s1==s2);

    String s3 = "hel" + "lo";
    // true
    log.info("s1==s3?{}", s1==s3);

    String s4 = new String("hello");
    // false
    log.info("s1==s4?{}", s1==s4);

    String s5 = "hel" + new String("lo");
    // false
    log.info("s1==s5?{}", s1==s5);

    String s6 = "hel";
    String s7 = "lo";
    String s8 = s6 + s7;
    // false
    log.info("s1==s8?{}", s1==s8);

    String s9 = new String("hello").intern();
    // true
    log.info("s1==s9?{}", s1==s9);
}

当以字面量形式创建字符串对象时,JVM会在编译期将其放到字符串常量池中,在Java启动时就加载到内存中了,这些字符串常量有且只有一份。

2.2 包装类中的享元模式应用

我们以Integer为例进行说明:

@Slf4j
public class IntegerTest {
    public static void main(String[] args) {
        Integer a1 = Integer.valueOf(100);
        Integer a2 = 100;
        // true
        log.info("a1==a2?{}", a1==a2);

        Integer b1 = Integer.valueOf(1000);
        Integer b2 = 1000;
        // false
        log.info("b1==b2?{}", b1==b2);
    }
}

之所以有这样的结果,是因为包装类Integer对某些常用范围内的整数进行了缓存:

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

所以,在缓存范围内的整数,都是直接从缓存中取出,而非创建一个新的对象。如此可以大大节省内存空间。

同理,其它类型的包装类也都有类似的机制。

三、模式总结

3.1 优点

  • 可以减少相同/相似对象的创建和销毁,节省了内存开销;

3.2 缺点

  • 需要区分内部状态和外部状态,增加了代码的复杂性;

相关文章

  • 享元模式及其应用

    一、模式介绍 享元模式(Flyweight)是一种对象池的实现方式,就像我们用过的线程池、数据库连接池等一样,主要...

  • 享元模式C++

    享元模式,就是运用共享技术有效地支持大量细粒度的对象。 享元模式结构图 享元模式基本代码 应用场景 享元模式可以避...

  • 设计模式 | 享元模式及典型应用

    前言 本文的主要内容: 介绍享元模式 示例-云盘 总结 源码分析享元模式的典型应用String中的享元模式Inte...

  • 55 - 享元模式之Integer、String实现剖析

    本文将剖析一下,享元模式在 Java Integer、String 中的应用 享元模式在 Java Integer...

  • Flyweight 享元模式(结构型模式)

    介绍   说到享元模式,第一个想到的应该就是池技术了,数据库连接池、缓冲池等等都是享元模式的应用,所以说享元模式是...

  • 享元模式

    享元模式(Flyweight),运用共享技术有效地支持大量细刻度的对象。享元模式是池技术的重要实现,可以减少应用程...

  • 享元模式(Flyweight)

    [转自]设计模式 | 享元模式及典型应用[https://juejin.cn/post/6844903683860...

  • 设计模式之享元模式(flyweight模式)

    引入享元模式 享元模式的实例 享元模式的分析 引入享元模式 flyweight是轻量级的意思,指的是拳击比赛中选手...

  • 第4章 结构型模式-享元模式

    一、享元模式的简介 二、享元模式的优缺点 三、享元模式的实例

  • JavaScript享元模式与性能优化

    摘要 享元模式是用于性能优化的设计模式之一,在前端编程中有重要的应用,尤其是在大量渲染DOM的时候,使用享元模式及...

网友评论

      本文标题:享元模式及其应用

      本文链接:https://www.haomeiwen.com/subject/pnwcvrtx.html