54

结构型 --面试高频之享元模式

 4 years ago
source link: http://www.cnblogs.com/zhxiansheng/p/12492494.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

享元模式是非常常用的一种结构性设计模式。

特别是在面试的时候。当我们把这一节内容掌握,我相信不管是工作中还是面试中这一块内容绝对是一大亮点。

什么是享元模式

所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

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

这里值得注意的是只保留一份实例,供多人使用。

面试最常见的面试题

我相信大伙在面试的时候经常会被问到String,Integer相关的面试题。

那我们就从这两块内容开始讲解。

享元模式在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

我相信很多人在面试的时候会遇到这种题目。答案可能会出乎我们的意料。第一个为true,第二个为false。

这正是因为 Integer,用到了享元模式来复用对象,才导致了这样的运行结果。当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。看代码更加清晰一些,Integer 类的 valueOf() 函数的具体代码如下所示:

//从这里的源码我们能看到,当我们执行Integer i2 = 56;
//这行代码的时候。其实是通过自动装箱机制,调用的valueOf。
//当数据在IntegerCache.low~IntegerCache.high之间的时候,我们是直接从缓存中拿取的数据。
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

那这个IntegerCache是什么呢?这个其实是Integer的内部类。

我们挑选重点代码来看看,源码如下:

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

这个是Integer的静态内部类,当我们加载Ineger的时候该类也会被加载进去。可以看到他缓存了-128 到 127 之间的整型值。

实际上,除了 Integer 类型之外,其他包装器类型,比如 Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。比如,Long 类型对应的 LongCache 享元工厂类及 valueOf() 。

其实jdk考虑的很周到,我们大部分时间创建出来的Ineger对象,其实都是存储整型都不是特别大。所以干脆取一段大小合理的数据直接缓存下来。

举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配 256 个 Integer 对象的内存空间。

享元模式在String中的应用

我们都知道String是被final修饰的,大家又仔细想过这其中的缘由吗?

这最大的原因就是为了实现字符串池化技术。其核心思想就是享元模式。

我们前面提到过享元对象都是不可变的。这样我们才能保证大家在共同使用的时候不会出现问题。所以String是被final修饰的。

我们再来看一下这段代码:

String s1 = "享元模式";
String s2 = "享元模式";
String s3 = new String("享元模式");

System.out.println(s1 == s2); //ture
System.out.println(s1 == s3); //false

前两个s1和s2都是指向的字符串常量池的"享元模式"。而s3指向的是堆的String。

String 类的享元模式的设计,跟 Integer 类稍微有些不同。

Integer 类中要共享的对象,是在类加载的时候,就集中一次性创建好的。

但是,对于字符串来说,我们没法事先知道要共享哪些字符串常量,所以没办法事先创建好。

只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需要再重新创建了

实际运用

我们想想,什么情况我们应该使用享元模式。

我总结了一下:

  1. 首先这个对象在很多地方都得使用,否则就是过度设计。
  2. 其次这个对象是不可变的,可以让多个线程同时使用。

我举一个具体的例子。

比如我们开发一个麻将游戏。没一局游戏是不是要new一个麻将桌,new一副麻将。假如同时在线100w人,那我们就new了25w个麻将桌和25w副麻。

我们仔细想想能不能用享元模式来优化,首先麻将桌应该是不能优化的,因为他得记录我们每一局游戏得状态,桌上麻将的情况,等等信息。但是麻将我们却可以缓存一副,让他不可变。所有人共用这一副缓存的麻将。

总结

享元模式其实开发中我们用的不是特别多,但是当需要时,却非常的有效。包括面试中关于String,基本类型的包装类关于享元模式的运用。当面试管再抛出这个问题,如果你能回答清楚并且提出其设计模式是享元模式,我相信一定会让面试官眼前一亮。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK