IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式之美-课程笔记35-享元模式

    10k发表于 2023-09-04 00:00:00
    love 0

    享元模式

    如何利用享元模式优化文本编辑器内存占用

    原理和实现

    1. 享元:被共享的单元。

    2. 享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

    3. 当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们可以利用享元模式将对象设计成享元,内存中只保留一份实例,供多处代码引用。(减少对象数量,节省内存)。

    4. 不可变对象指的是:一单通构造函数初始化完成之后,他的状态(对象的成员变量或者属性)就不会再被修改了。所以不可变对象不能暴露setter等修改内部状态的方法。

    5. 来个例子:

      假设我们在开发一个棋牌游戏(比如象棋)。一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 30 个棋子的信息。

    public class ChessPiece {//棋子
      private int id;
      private String text;
      private Color color;
      private int positionX;
      private int positionY;
    
      public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
        this.id = id;
        this.text = text;
        this.color = color;
        this.positionX = positionX;
        this.positionY = positionX;
      }
    
      public static enum Color {
        RED, BLACK
      }
    
      // ...省略其他属性和getter/setter方法...
    }
    
    public class ChessBoard {//棋局
      private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
    
      public ChessBoard() {
        init();
      }
    
      private void init() {
        chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
        chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
        //...省略摆放其他棋子的代码...
      }
    
      public void move(int chessPieceId, int toPositionX, int toPositionY) {
        //...省略...
      }
    }
    
    1. 游戏大厅有许许多多的对局,如果对于每个棋局上每个棋子都创建新的对象,那将是非常庞大的对象数量。这里可以利用享元模式,将棋子的信息抽离出来设计为独立的类,作为享元供多个棋盘使用。棋盘只记录棋子的位置信息即可。
    // 享元类
    public class ChessPieceUnit {
      private int id;
      private String text;
      private Color color;
    
      public ChessPieceUnit(int id, String text, Color color) {
        this.id = id;
        this.text = text;
        this.color = color;
      }
    
      public static enum Color {
        RED, BLACK
      }
    
      // ...省略其他属性和getter方法...
    }
    
    public class ChessPieceUnitFactory {
      private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
    
      static {
        pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
        pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
        //...省略摆放其他棋子的代码...
      }
    
      public static ChessPieceUnit getChessPiece(int chessPieceId) {
        return pieces.get(chessPieceId);
      }
    }
    
    public class ChessPiece {
      private ChessPieceUnit chessPieceUnit;
      private int positionX;
      private int positionY;
    
      public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
        this.chessPieceUnit = unit;
        this.positionX = positionX;
        this.positionY = positionY;
      }
      // 省略getter、setter方法
    }
    
    public class ChessBoard {
      private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
    
      public ChessBoard() {
        init();
      }
    
      private void init() {
        chessPieces.put(1, new ChessPiece(
                ChessPieceUnitFactory.getChessPiece(1), 0,0));
        chessPieces.put(1, new ChessPiece(
                ChessPieceUnitFactory.getChessPiece(2), 1,0));
        //...省略摆放其他棋子的代码...
      }
    
      public void move(int chessPieceId, int toPositionX, int toPositionY) {
        //...省略...
      }
    }
    
    1. 在上述代码中,我们利用工厂类来缓存ChessPieceUnit心选,通过工厂类获取到的就是享元。(对比使用享元模式之前,一万个棋局,每个棋局都是30个棋子那就要创建30万个对象,而使用享元模式之后,只需要30个对象即可)。
    2. 代码结果: 通过工厂模式,在工厂类中,通过一个Map来缓存已经创建过的享元对象,以达到复用目的。

    享元模式在文本编辑器中的应用

    1. 我们要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。
    2. 文本编辑器可以给每个文字设置不同的格式,我们需要将每个文字当成一个对象来看待并且记录他的格式:
    public class Character {//文字
      private char c;
    
      private Font font;
      private int size;
      private int colorRGB;
    
      public Character(char c, Font font, int size, int colorRGB) {
        this.c = c;
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
      }
    }
    
    public class Editor {
      private List<Character> chars = new ArrayList<>();
    
      public void appendCharacter(char c, Font font, int size, int colorRGB) {
        Character character = new Character(c, font, size, colorRGB);
        chars.add(character);
      }
    }
    
    1. 同样的问题,如果是许多文字那这个实现将会创建许多对象。这里也可以用到享元。
    2. 在一个文本文件中,用到的字体格式不会太多,毕竟不大可能有人把每个文字都设置成不同的格式。所以对于字体格式我们可以将他设计成享元,让不同的文字使用。
    public class CharacterStyle {
      private Font font;
      private int size;
      private int colorRGB;
    
      public CharacterStyle(Font font, int size, int colorRGB) {
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
      }
    
      @Override
      public boolean equals(Object o) {
        CharacterStyle otherStyle = (CharacterStyle) o;
        return font.equals(otherStyle.font)
                && size == otherStyle.size
                && colorRGB == otherStyle.colorRGB;
      }
    }
    
    public class CharacterStyleFactory {
      private static final List<CharacterStyle> styles = new ArrayList<>();
    
      public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
        CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
        for (CharacterStyle style : styles) {
          if (style.equals(newStyle)) {
            return style;
          }
        }
        styles.add(newStyle);
        return newStyle;
      }
    }
    
    public class Character {
      private char c;
      private CharacterStyle style;
    
      public Character(char c, CharacterStyle style) {
        this.c = c;
        this.style = style;
      }
    }
    
    public class Editor {
      private List<Character> chars = new ArrayList<>();
    
      public void appendCharacter(char c, Font font, int size, int colorRGB) {
        Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
        chars.add(character);
      }
    }
    

    享元模式vs单例、缓存、对象池

    区别设计模式看的是设计意图和应用场景。

    1. 单例中一个类只能创建一个对象,享元中一个类可创建多个对象,并在多处被共享(有点像多例)。单例是为了限制对象的个数,享元是为了对象复用,节省内存。
    2. 缓存是为了提高访问效率而非复用。
    3. 对象池: 为了避免频繁地进行对象创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。
      1. 池化技术的复用是重复使用,还是为了节省时间(连接数据库)。一个时刻这个线程、连接、对象都是被单独的使用者占用。
      2. 享元模式的复用是共享使用。多个使用者同一时间可以使用同一个对象。

    剖析享元模式在Java Integer、String中的应用

    享元模式在Java Integer中的应用

    Integer i1 = 56;
    Integer i2 = 56;
    Integer i3 = 129;
    Integer i4 = 129;
    System.out.println(i1 == i2); // true;
    System.out.println(i3 == i4); // false;
    
    1. 要判断上面代码的输出结果,需要知道三个知识点:
      1. 如何判断Java对象是否相等(”==“)
      2. 自动装箱和自动拆箱
      3. 享元模式
    2. Java为基本数据类型提供对应的包装器类型,所谓自动装箱就是将基本数据类型转换为包装器类型。反之就是自动拆箱。
    Integer i = 56; //自动装箱
    int j = i; //自动拆箱
    
    1. 数值56是基本数据类型int,当赋值给包装器类型Integer的时候,出发装箱操作,创建一个Integer类型的对象,并赋值给变量i。其底层相当于执行了下面:
    Integer i = 59;
    // 底层执行了:
    Integer i = Integer.valueOf(59);
    
    1. 反之当包装器类型变量i,复制给基本变量类型j的时候,触发自动拆箱,将i中的数据提取出赋值给j
    int j = i; 
    // 底层执行了:
    int j = i.intValue();
    
    1. 而Java对象在内存中存储中存储方式为:

      img

    2. 当用双等号==判断两个对象相等时,判断的是他们的地址是否指向同一个位置。所以开头的代码你可能会判断都是false。但事实上第一个判断并不是。

    3. Integer用到享元模式来复用对象。当我们自动装箱创建integer对象的时候,如果值在-128~127之间,会从IntegerCache类中直接返回。否则才调用new方法创建。
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    

    IntegerCache源码:

    /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */
    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() {}
    }
    
    1. 为什么只有-127~128ze这个范围呢? 因为整数太多了,不可能吧所有数字做成这样,只选择常用的整数,也就是一个字节的大小。
    2. JDK还允许通过property配置最大值。
    3. 另外,Long、Short、Byte也都有类似的方法利用享元模式缓存-128~127之间的数据。
    4. 平时使用可以优先使用后两种创建方式以利用享元模式:
    Integer a = new Integer(123);
    Integer a = 123;
    Integer a = Integer.valueOf(123);
    

    享元模式在Java String中的应用

    String s1 = "小争哥";
    String s2 = "小争哥";
    String s3 = new String("小争哥");
    
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);
    
    1. 和Integer一样的思路,String类会利用享元模式复用相同的字符串常量。JVM会专门开辟一款存储区来存储字符串常量,叫做:字符串常量池。

      img

    2. 和Integer不同的是,String没办法事先知道哪些字符串要被创建和使用,所以在第一次使用到的时候还是先创建并加到常量池,后续使用就使用常量池中的对象即可。

    3. 字符串常量池是一个固定大小的HashTable,哈希表,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长。


沪ICP备19023445号-2号
友情链接