享元模式在 Java Integer 中的应用

自动装箱与自动拆箱

Java中的基本数据类型对应的有包装器类型,他们之前存在着自动装箱和拆箱的过程。

所谓的自动装箱,就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱,也就是自 动将包装器类型转化为基本数据类型。具体的代码示例如下所示:

1
2
Integer i = 56; //自动装箱
int j = i; //自动拆箱

数值 56 是基本数据类型 int,当赋值给包装器类型(Integer)变量的时候,触发自动装箱 操作,创建一个 Integer 类型的对象,并且赋值给变量 i。其底层相当于执行了下面这条语句:

1
Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);

反过来,当把包装器类型的变量 i,赋值给基本数据类型变量 j 的时候,触发自动拆箱操 作,将 i 中的数据取出,赋值给 j。其底层相当于执行了下面这条语句:

1
int j = i; 底层执行了:int j = i.intValue();

对象存储

1
User a = new User(123, 23); // id=123, age=23
image-20230616110300848

当我们通过“==”来判定两个对象是否相等的时候,实际上是在判断两个局部变量存储的 地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象。

一个具体的例子

看下面的代码,它的结果是什么样的?

1
2
3
4
5
6
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

前 4 行赋值语句都会触发自动装箱操作,也就是会创建 Integer 对象并且赋值给 i1、i2、 i3、i4 这四个变量。i1、i2 尽管存储的数值相同,都是 56,但是指向不同的 Integer 对象,所以通过“==”来判定是否相同的时候,会返回 false,同理第二个也是false。这样对吗?

答案并非是两个 false,而是一个 true,一个 false。

实际上,这正是因为 Integer 用到了享元模式来复用对象,才 导致了这样的运行结果。当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中 直接返回,否则才调用 new 方法创建。

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

因为 56 处于 -128 和 127 之间,i1 和 i2 会指向相同 的享元对象,所以 i1==i2 返回 true。而 129 大于 127,并不会被缓存,每次都会创建一 个全新的对象,也就是说,i3 和 i4 指向不同的 Integer 对象,所以 i3==i4 返回 false。

在平时开发中,优先使用后两种创建:

1
2
3
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);

第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用 IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。

享元模式在 Java String 中的应用

1
2
3
4
5
String s1 = "aaa";
String s2 = "aaa";
String s3 = new String("aaa");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

上面代码的运行结果是:一个 true,一个 false。String 类 利用享元模式来复用相同的字符串常量,JVM 会专门开辟 一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。

不过,String 类的享元模式的设计,跟 Integer 类稍微有些不同。Integer 类中要共享的对 象,是在类加载的时候,就集中一次性创建好的。但是,对于字符串来说,我们没法事先知 道要共享哪些字符串常量,所以没办法事先创建好,只能在某个字符串常量第一次被用到的 时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需 要再重新创建了。

参考

《设计模式之美》