• 【设计模式】结构型设计模式之 享元模式


    介绍

    享元模式,”享元“ 就是被共享的单元。享元模式的意图就是复用对象节省内存,应用的前提是被共享的对象是不可变的对象。
    将对象设计成享元,保留一份实例供多处代码引用这样能减少内存中对象的数量,不允许修改是因为避免一出修改影响其他使用他的代码。

    关键概念

    1. 享元(Flyweight):这是模式中的核心对象,可以被多个客户端共享。享元对象需要保持内部状态(Internal State)的共享,而外部状态(External State)则由客户端在使用时传入。
    2. 内部状态(Internal State):存储在享元对象内部,可以被共享,不随环境改变而改变的状态。
    3. 外部状态(External State):随环境改变而改变、不能共享的状态,由客户端传入享元对象,以便在运行时根据外部状态来区分不同的享元实例。
    4. 工厂(Factory):负责创建和管理享元对象,确保有效地复用享元对象,通常会使用缓存来存储已经创建的享元对象,以避免重复创建。

    应用举例

    象棋游戏共享棋子对象

    问题:如果每个棋子都包含 id、文本、颜色、横坐标、纵坐标属性。并且每个游戏房间都有一个棋盘,那么如果游戏大厅中有成千上万个棋盘和对应的棋子。那么将创建大量的棋子对象。
    方案:利用享元模式,象棋的棋子的一些属性,例如 id、颜色、文本都是固定不变的,下棋时每个棋盘的棋子也是一样的。所以只需要让棋子对象中保存 id、颜色、文本后被所有棋盘共享即可。每个房间只需要保存棋子的 id 和位置。这样就能节省大量的内存。

    文本编辑器中文字格式设计成享元模式

    一个文本编辑器,每个输入的字符都可以单独调整文本样式,如果给每个字符都保存一个字符样式对象那么十分浪费内存。并且文本编辑器中,一篇文本往往只有少量的几个文本格式所以设计成享元模式能节省大量内存。

    /**
     * 文本样式类
     *
     * @author Jean
     * @date 2024/06/04
     */
    @Getter
    public class CharsetStyle {
        private int font;
        private int fontSize;
        private int colorRgb;
    
        public CharsetStyle(int font, int fontSize, int colorRgb) {
            this.font = font;
            this.fontSize = fontSize;
            this.colorRgb = colorRgb;
        }
    
    
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof CharsetStyle)) {
                return false;
            }
            CharsetStyle other = (CharsetStyle) obj;
            return other.font == this.font && this.fontSize == other.fontSize && this.colorRgb == other.colorRgb;
        }
    
    
        /**
         * 重写了equals一般要重写hashcode方法
         * 1. 如果两个对象根据 equals 方法判断是相等的,那么它们的 hashCode 方法必须返回相同的值。
         * 2. 如果两个对象根据 equals 方法判断是不相等的,那么它们的 hashCode 方法不必返回不同的值,但建议这样做以提高散列表(如 HashMap)的性能。
         * 违反这些规则可能会导致你的类在集合(尤其是基于散列的集合,如 HashSet 和 HashMap)中的行为不符合预期,比如无法正确识别已存在的元素或者影响集合的性能。因此,重写 equals 时配套重写 hashCode 是一种最佳实践。
         *
         * @return int
         */
        @Override
        public int hashCode() {
            // 使用常数乘法、位移等操作来组合字段,以生成独特的哈希值
            int result = 17;
            result = 31 * result + font;
            result = 31 * result + fontSize;
            result = 31 * result + colorRgb;
            return result;
        }
    }
    
    
    /**
     * 文本样式工厂
     *
     * @author Jean
     * @date 2024/06/04
     */
    public class CharacterStyleFactory {
    
    
        /**
         * 用来共享的文本样式
         */
        private static final Set<CharsetStyle> styles = ConcurrentHashMap.newKeySet();
    
    
        /**
         * 获取样式的工厂
         *
         * @param font
         * @param fontSize
         * @param colorRgb
         * @return {@link CharsetStyle}
         */
        public static CharsetStyle getCharsetStype(int font, int fontSize, int colorRgb) {
            CharsetStyle charsetStyle = new CharsetStyle(font, fontSize, colorRgb);
            for (CharsetStyle style : styles) {
                if (style.equals(charsetStyle)) {
                    return style;
                }
            }
            styles.add(charsetStyle);
            return charsetStyle;
        }
    
    
    }
    

    享元模式在 Java 中的应用

    享元模式在包装类缓存中的应用

    Integer i1=56;
    Integer i2=56;
    Integer i3=129;
    Integer i4=129;
    System.out.pringln(i1==i2); //输出true
    System.out.pringln(i3==i4); //输出false
    

    例如上面的代码,会先输出 true 再输出 false

    1. i1 和 i2 自动装箱实际上调用的是 Integer.valueOf()方法,对应的自动拆箱的时候实际上调用的 Integer.intValue()方法。
    2. 在 Integer.valueOf 方法中就用到了享元模式,这个方法中实际上是从一个 Integer 的内部类 IntegerCache.cache 中获取缓存好的 Integer 对象;
      1. IntegerCache 实际上就是 Integer 的一个工厂类虽然没有以 Factory 结尾。
      2. IntegerCache 中缓存了 -128~127 的 Integer 对象,如果调用 Integer.value 的 int 值在其范围内,则会直接从 Cache 中返回。
      3. Integer 对象和其他基本类型的包装对象都是不可变的对象。

    为什么默认是 -128~127 这个范围可调吗?

    1. 因为预先创建过多的对象,会占用内存并且导致加载时间延长所以只能在一定范围内缓存 所以缓存了 1 个字节大小的整形值。
    2. 可以调 -Djava.lang.Integer.Cache.high=255 或者 -XX:AutoBoxCacheMax=255

    Long、Short、Byte 等基础类型的包装类型都有缓存对应范围的对象

    享元模式在 String 中的应用

    在 Java 中 有一个字符串常量池,在加载时一些字符串就会被创建到常量池中。后续引用到相同的字符串则可以从常量池中直接获取。这也是享元模式的一种应用。

    对比

    享元模式和单例模式的区别

    1. 单例模式中一个类只能创建一个对象,享元模式中是创建多个对象后被多处代码共享。
    2. 享元模式有点像单例模式的变体,多例模式,但是仍然有很大的区别。区别在和多例模式的设计意图上
    3. 多例模式单例模式都是意在控制对象的数量,而享元模式的意图是对象共享

    享元模式与缓存的区别

    1. 享元模式通过工厂来“缓存”创建好的对象,但是这里的缓存更多的意思是存储。
    2. 缓存系统是为了提高访问效率而存在的,而享元模式只是为了复用。

    总结

    应用享元模式前应该仔细测试是否真的在业务场景中能节省大量内存,否则可能适得其反。

    优点

    1. 享元模式在对象被密集使用,并且内容不变时能在多处共享节省大量内存

    缺点

    1. 对垃圾回收不友好,因为共享的对象一保有引用不会释放。
    2. 如果对象的生命周期很短并且不会被密集使用,使用享元模式可能占用更多的内存。
  • 相关阅读:
    妈妈再也不用担心我搞不懂——log4j/logback/log4j2原理了
    程序员35岁之后如何规划?建议收藏!
    cocos2dx查看版本号的方法
    getActionBar()=null的问题
    consul 备份还原导入导出
    Mybatis-plus的操作(新增,修改,删除)
    newstar week3 pwn
    react-redux基本使用
    2023前端大厂高频面试题之CSS篇(2)
    Java 继承(extends)
  • 原文地址:https://blog.csdn.net/weixin_40979518/article/details/139553551