自学内容网 自学内容网

重修设计模式-结构型-享元模式

重修设计模式-结构型-享元模式

复用不可变对象,节省内存

享元模式(Flyweight Pattern)核心思想是通过共享对象方式,达到节省内存和提高性能的目的。享元对象需是不可变对象,因为它会被多处代码共享使用,要避免一处代码对享元进行了修改,影响到其他使用它的代码。

享元模式的实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建过的享元对象,来达到复用的目的。

举个例子,在线象棋游戏:

象棋可同时容纳上万个房间同时进行游戏,每个房间最基础的是象棋和棋盘。一副象棋有32个棋子,如果后每个房间都创建相同的棋子对象,就是千万级别的,对任何系统都是个挑战。

分析这个例子,棋子的字和颜色是固定的几个,跟场景无关;场景相关的只有只有棋子的坐标。那其实可以把棋子不变的部分抽取出来,设计为享元对象,让所有棋子共享,从而让每个棋子对象变得更轻量。

未使用享元模式的棋子对象:

//棋子
data class ChessPieceOld(
    val id: Long,
    val text: String,
    val color: ChessPieceUnit.Color,
    val positionX: Int,
    val positionY: Int
) {
}

当对象大量创建时,几个固定的属性(id、text、color)会大量重复,冗余占用内存。这时可以将这几个属性抽出,设计为享元类,并使用带缓存的工厂来生成享元对象:

//棋子固定信息-享元类
data class ChessPieceUnit(val id: Long, val text: String, val color: Color) {
    enum class Color {
        RED, BLACK
    }
}

//生成棋子-带缓存的创建工厂
class ChessPieceFactory {
    companion object {
        //享元对象池
        private val chessPiece = hashMapOf(
            1 to ChessPieceUnit(1, "車", ChessPieceUnit.Color.RED),
            2 to ChessPieceUnit(2, "馬", ChessPieceUnit.Color.BLACK),
            //...
        )

        fun getChessPiece(id: Int): ChessPieceUnit {
            return chessPiece[id]!!
        }
    }
}

//棋子
data class ChessPiece(val piece: ChessPieceUnit, val positionX: Int, val positionY: Int) {
}

//棋盘
class ChessBoard() {
    private val chessPieces: HashMap<Int, ChessPiece> = hashMapOf()

    fun init() {
        chessPieces.put(1, ChessPiece(ChessPieceFactory.getChessPiece(1), 0, 1))
        chessPieces.put(2, ChessPiece(ChessPieceFactory.getChessPiece(1), 0, 1))
        //...
    }

    fun move(chessPieceId: Int, positionX: Int, positionY: Int) {
        //...
    }
}

使用享元后,所有棋子的固定部分共同引用数量有限的享元对象,每个棋子对象变得更为轻量,内存中存储的冗余信息大大减少,避免了大量相似对象的开销,提高了系统资源的利用率。

注意上面的例子,使用享元模式后,棋子对象的数量并没有变化,只是对象比之前小了很多。消耗内存最多的成员变量已经被移动到很少的几个享元对象中了,这几个享元对象会被上千个情境小对象复用,无需再重复存储相同数据。

享元模式在 Java 语言中的应用

1.Java 中的字符串常量池

String 类会利用享元模式来复用相同的字符串常量,当某个字符串常量第一次被用到的时候,存储到常量池中,之后再用到的时候,直接引用常量池中已经存在的即可,不需要重复创建对象,通过下面代码可以验证。

//字符串常量池
String s1 = "秋意浓";
String s2 = "秋意浓";
String s3 = new String("秋意浓");//直接new出对象,绕过了Java的常量池优化
System.out.println(s1 == s2);   //输出:true
System.out.println(s1 == s3);   //输出:false

2.基本类型包装类

基本类型的包装类(如Integer 、Long、Short、Byte 等),也都利用了享元模式来缓存 -128 到 127 之间的数据。因为对于大部分应用来说这个区间是最常用的数值,如果预先创建所有值的享元对象不仅会占用大量内存,也会让类加载时间过长。

以 Integer 类型为例:

//包装类型的享元
Integer n1 = 1;
Integer n2 = 1;
Integer n3 = 129;
Integer n4 = 129;
System.out.println(n1 == n2);   //输出:true
System.out.println(n3 == n4);   //输出:false

因为 129 超出了享元对象池区间,所以每次会返回新的对象,两个对象地址不同,输出 false。Integer 享元部分源码如下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
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() {}
}

可以通过 JDK 参数修改缓存池上限(下限不支持修改):

//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255

在使用 Java 进行编码时,要尽量避免直接 new 出包装类型或者字符类型对象,如 String s = new String("aaa")Integer n = new Integer(1),这样会绕过系统的享元模式,创建重复对象。推荐直接使用自动装箱语法,如:Integer n = 1,或通过类型工厂的方式:Integer n = Integer.valueof(1)

上面两个场景都是享元模式在 Java 中的应用,只是实现略有不同:Integer 类中要共享的对象,是在类加载的时候就一次性创建好的;String 类的享元,是在某个字符串第一次被用到的时候,才创建并存储到常量池中的,相当于懒加载方式。

享元模式的类似设计

1.享元模式 vs 单例

虽然享元模式的实现和单例的变体多例非常相似,但它们的设计意图不同:享元模式是为了对象复用,节省内存。而多例是为了限制对象的个数。

2.享元模式 vs 对象池

虽然享元模式和对象池都是为了复用,但他们的“复用”也是不同的概念:

  • 对象池中的“复用”可以理解为“重复使用”,使用时被使用者独占,使用完成后放回池中,主要目的是节省时间。
  • 享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

总结

当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。


原文地址:https://blog.csdn.net/weixin_39397471/article/details/142461769

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!