「设计模式」亨元模式

182 阅读4分钟

一、概述

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。它提供了减少对象数量从而改善应用所需的对象结构的方式。

说起享元模式,最容易想到的是单例模式,它们之间也有着很相似的地方,这两种模式有什么区别呢?

(1)享元模式可以多次创建对象,也可以从共享缓存取对象。

(2)单例模式是严格控制单个进程只有一个实例对象。

(3)享元模式可以通过自己实现对外部的单例,也可以在需要的时候创建更多的对象。

个人理解,两种模式目的都是为了节省内存开销,享元模式可以看成单例模式的一种拓展。

为了方便理解,我们先来看一下享元模式的两种状态:

  • 内部状态(Intrinsic State):是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
  • 外部状态(Extrinsic State):是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。

享元模式的4种角色:

  • Flyweight(抽象享元类):接口或抽象类,声明公共方法,这些方法可以向外界提供对象的内部状态,设置外部状态。
  • ConcreteFlyweight(具体享元类):实现了抽象享元类,其实例称为享元对象。必须是可共享的,需要封装享元对象的内部状态;。
  • UnsharedConcreteFlyweight(非共享具体享元类):非共享的享元实现对象,并不是所有的享元对象都可以共享,非共享的享元对象通常是享元对象的组合对象。
  • FlyweightFactory(享元工厂类):享元工厂,主要用来创建并管理共享的享元对象,并对外提供访问共享享元的接口。它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。

二、优缺点

优点:大大减少对象的创建,降低系统的内存,使效率提高。

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

三、实现方式

享元模式的简单实现

抽象享元对象类

public abstract class Flyweight {
    // 内部状态
    private String instrinsic;

    // 外部状态
    protected String extrinsic;

    protected Flyweight(String extrinsic) {
        this.extrinsic = extrinsic;
    }

    // 业务操作
    public abstract void operate();

    public String getInstrinsic() {
        return instrinsic;
    }

    public void setInstrinsic(String instrinsic) {
        this.instrinsic = instrinsic;
    }
}

具体享元对象类

public class ConcreteFlyweight extends Flyweight{

    protected ConcreteFlyweight(String extrinsic) {
        super(extrinsic);
    }

    @Override
    public void operate() {
        System.out.println(extrinsic);
    }
}

享元工厂类

public class FlyweightFactory {
    // 池子
    private static HashMap<String,Flyweight> pool = new HashMap<>();

    // 获取共享对象
    public static Flyweight getFlyweight(String extrinsic){
        // 返回共享对象
        Flyweight flyweight = null;
        // 池中没有该对象
        if(pool.containsKey(extrinsic)){
            flyweight = pool.get(extrinsic);
        }else{
            // 根据外部状态创建享元对象
            flyweight = new ConcreteFlyweight(extrinsic);
            // 回收到池中
            pool.put(extrinsic,flyweight);
        }
        return flyweight;
    }

    public static void show(){
        System.out.println("当前共享对象数:" + pool.size());
    }

}

客户端

public class Client {
    public static void main(String[] args) {
        FlyweightFactory flyweightFactory = new FlyweightFactory();
        FlyweightFactory.show();

        Flyweight flyweight = flyweightFactory.getFlyweight("共享对象1");
        FlyweightFactory.show();
        flyweight.operate();

        flyweight = flyweightFactory.getFlyweight("共享对象2");
        FlyweightFactory.show();
        flyweight.operate();

        flyweight = flyweightFactory.getFlyweight("共享对象1");
        FlyweightFactory.show();
        flyweight.operate();
    }
}

结果输出

当前共享对象数:0
当前共享对象数:1
共享对象1
当前共享对象数:2
共享对象2
当前共享对象数:2
共享对象1

String常量池

Java String常量池的应用:对于创建过的String,直接指向调用即可,不需要重新创建。

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
System.out.println(str1 == str2); // true
System.out.println(str3 == str4); // false

Java包装类

在Java中有Short、Long、Byte、Integer等包装类。这些类中都用到了享元模式,以Integer 为例进行讲解。

Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2); // true
System.out.println(i3 == i4); // false

上述结果是因为Integer包装类型的自动装箱和拆箱、Integer中的享元模式的结果导致的。

直接上源码

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() {}
    }

valueOf


/**
     * 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);
    }

四、常见应用场景

  • 系统中存在大量的相似对象。
  • 需要缓冲池的场景。