前言
享元模式是非常常用的一种结构性设计模式.
特别是在面试的时候. 当我们把这一节内容掌握, 我相信不管是工作中还是面试中这一块内容绝对是一大亮点.
什么是享元模式
所谓 "享元", 顾名思义就是被共享的单元. 享元模式的意图是复用对象, 节省内存, 前提是享元对象是不可变对象.
具体来讲, 当一个系统中存在大量重复对象的时候, 如果这些重复的对象是不可变对象, 我们就可以利用享元模式将对象设计成享元, 在内存中只保留一份实例, 供多处代码引用. 这样可以减少内存中对象的数量, 起到节省内存的目的.
这里值得注意的是只保留一份实例, 供多人使用.
面试最常见的面试题
我相信大伙在面试的时候经常会被问到 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 类中要共享的对象, 是在类加载的时候, 就集中一次性创建好的.
但是, 对于字符串来说, 我们没法事先知道要共享哪些字符串常量, 所以没办法事先创建好.
只能在某个字符串常量第一次被用到的时候, 存储到常量池中, 当之后再用到的时候, 直接引用常量池中已经存在的即可, 就不需要再重新创建了
实际运用
我们想想, 什么情况我们应该使用享元模式.
我总结了一下:
首先这个对象在很多地方都得使用, 否则就是过度设计.
其次这个对象是不可变的, 可以让多个线程同时使用.
我举一个具体的例子.
比如我们开发一个麻将游戏. 没一局游戏是不是要 new 一个麻将桌, new 一副麻将. 假如同时在线 100w 人, 那我们就 new 了 25w 个麻将桌和 25w 副麻.
我们仔细想想能不能用享元模式来优化, 首先麻将桌应该是不能优化的, 因为他得记录我们每一局游戏得状态, 桌上麻将的情况, 等等信息. 但是麻将我们却可以缓存一副, 让他不可变. 所有人共用这一副缓存的麻将.
总结
享元模式其实开发中我们用的不是特别多, 但是当需要时, 却非常的有效. 包括面试中关于 String, 基本类型的包装类关于享元模式的运用. 当面试管再抛出这个问题, 如果你能回答清楚并且提出其设计模式是享元模式, 我相信一定会让面试官眼前一亮.
来源: https://www.cnblogs.com/zhxiansheng/p/12492494.html