简介
享元设计模式是GOF23中设计模式之一,主要是减少对象的创建,即共享对象,以减少内存占用和提高性能。其主要解决在有大量对象时,有可能会造成内存溢出,把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重复创建。
享元设计模式是一种创建型设计模式,它旨在减少程序中创建对象的数量,以提高程序的性能和效率。
在享元设计模式中,对象被分为两种类型:内部状态和外部状态。内部状态是对象的共享部分,外部状态是对象的可变部分。享元模式的主要思想是共享对象,即多个对象可以共享同一个对象实例,从而减少对象的创建和销毁次数。这种共享对象的方式可以通过对象池来实现,即将对象存储在池中,需要使用时从池中获取,使用完后放回池中。
享元模式适用于需要创建大量相似对象的场景,例如图像处理或游戏开发中的角色对象。
定义
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
UML
应用案例
在java程序中,有大量的“细颗粒度”的对象,如数字1、2、3,如每个都是独立的,则会占用大量内存。这是一个适合使用享元模式的典型应用场景,底层jdk是否是这样实现的呢?
我们通过几个测试来验证。
基本类型int示例
基本数据类型int的比较,如下:
int a=1;
int b=1;
System.out.println(a==b); //true
int是基础类型,==比较的是值是否相等,上面测试结果是true,没什么疑问。
整形包装类示例
我们再来看下整形包装类Integer的比较
Integer integer1=new Integer(100);
Integer integer2=new Integer(100);
System.out.println( integer1==integer2 ); //false
Java中的引用类型用“==”时比较的是地址,integer1与integer2地址不同,返回false。
注意这里是用Integer的构造方法赋值,并不是直接用等号。
再看下面的例子:
Integer integer3=20;
Integer integer4=20;
System.out.println( integer3==integer4 ); //true
既然==比较的地址,为什么以上结果又相同了呢?
原理
对于Integer,整形数值常用,为避免创建大量重复对象,占用大量内存,在这里使用了享元设计模式,也就是建立了常量池,像 “Integer 变量名=?” 这种形式定义的Integer变量,实际调用的是Integer.valueOf方法,会被放入常量池,当一个Integer变量放入常量池前会有一个判断,若常量池中存在和该变量值相等的变量,则两变量共用一块内存,否则将该变量存入变量池,单独分配内存。
上面的integer3和integer4的值相等,因此共用一块内存,“==”比较就返回true了。
这里就体现出了享元模式在Java中应用。
再看一个例子
Integer integer5=200;
Integer integer6=200;
System.out.println( integer5==integer6 ); //false
这段代码与上面的差不多的,为什么会返回false呢?
原因就是Integer 变量名=?这样定义的变量不会都被放入常量池,当变量的值在(-128,127)之间(也就是可以用一个字节所能表示的int值)时才会被放入常量池,否则会自动装箱生成普通Integer对象。
Integer源码如下:
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) { // must cache
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
private static class IntegerCache {
private IntegerCache(){}
static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
}
总结
通过上面一组测试,验证了我们开始的想法,即jdk使用享元模式,建立了常量池,从而避免了产生大量细粒度的对象,从而节省了内存开销和提高了性能。
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
延展
Java中对String类型实现了常量池(String Pool)或称为字符串池。这个常量池是Java堆内存中的一个特殊存储区域,用于存储字符串字面量。当创建一个字符串字面量时,JVM会首先检查字符串常量池中是否已经存在一个相同值的字符串对象,如果存在,则返回对该对象的引用;如果不存在,则会在字符串常量池中创建一个新的字符串对象,并返回对该对象的引用。
例如,以下代码:
String s1 = "hello";
String s2 = "hello";
这里s1和s2虽然是两个不同的引用,但它们指向的是字符串常量池中的同一个String对象。因此,s1 == s2的结果为true,因为它们在内存中的地址是相同的。
然而,需要注意的是,通过new关键字创建的String对象不会直接在字符串常量池中查找或共享。例如:
String s3 = new String("hello");
在这种情况下,s3引用的是堆内存中新创建的String对象,而不是字符串常量池中的对象。因此,s1 == s3的结果为false,即使它们的内容相同。
此外,字符串常量池不仅限于字面量。当你调用intern()方法时,也可以将一个String对象添加到字符串常量池中(如果它尚未存在):
String s4 = new String("hello");
s4 = s4.intern();
现在,s4引用的是字符串常量池中的String对象,因此s1 == s4的结果为true。
总的来说,Java中的字符串常量池是一个用于优化字符串对象创建和内存使用的机制。但是,由于它只针对字符串字面量和通过intern()方法显式添加的字符串,因此在使用new关键字创建字符串时,需要特别注意对象的引用和相等性比较。