设计模式 --面试高频之享元模式
前言
享元模式是非常常用的一种结构性设计模式。
特别是在面试的时候。当我们把这一节内容掌握,我相信不管是工作中还是面试中这一块内容绝对是一大亮点。
什么是享元模式
所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。
这里值得注意的是只保留一份实例,供多人使用。
面试最常见的面试题
我相信大伙在面试的时候经常会被问到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,基本类型的包装类关于享元模式的运用。当面试管再抛出这个问题,如果你能回答清楚并且提出其设计模式是享元模式,我相信一定会让面试官眼前一亮。
下一篇: 15.RDD 创建