当前位置:   article > 正文

设计模式学习笔记 - 设计模式与范式 -结构型:8.享元模式(下):享元模式在Java Integer、String中的应用

设计模式学习笔记 - 设计模式与范式 -结构型:8.享元模式(下):享元模式在Java Integer、String中的应用

概述

上篇文章《结构型:7.享元模式(上):享元模式原理和应用》,通过棋牌游戏和文本编辑器的例子,学习了享元模式的原理、实现以及应用场景。用一句话总结下,享元模式中的 “享元” 指被共享的单元。享元模式通过复用对象,已达到节省内存的目的。

本章,带你剖析一下享元模式在 Java Integer、String中的应用。


享元模式在 Java Integer 中的应用

先看下面这段代码。先思考下,这段代码会输出什么样的结果。

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

如果你不熟悉 Java,你可能会觉得 i1 和 i2 值都是 56,i3 和 i4 值都是 129,所以 i1 跟 i2 值相等, i3 跟 i4 值相等,所以输出的结果应该是两个 true,这个图的分析是不对的,主要还是因为你对 Java 语法不熟悉。要正确分析上面的代码,需要弄清楚下面两个问题。

  • 如何按断两个 Java 对象是否相等(也就是代码中 “==” 操作符的含义)?
  • 什么事自动装箱(Autoboxing)和自动拆箱(Unboxing)?

Java 为基本数据类型提供了对应地包装器类型,具体如下表所示。

基本数据类型对应地包装器类型
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean
shortShort
byteByte
charCharacter

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

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

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

Integer i = 56; 底层执行了 Integer i = Integer.valueOf(56);
  • 1

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

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

弄清楚了自动装箱和拆箱,再看下,如何判定两个类型是否相等? 在此之前,我们要先搞清楚,Java 对象在内存中是如何存储的。通过下面的例子来说明下。

User a = new User(123,123);
  • 1

针对这条语句,我画了一张内存存储结构图,如下所示。 a 存储的值是 User 对象的内存地址,在图中表现为 a 指向 User 对象。

在这里插入图片描述
通过 “==” 来判定两个对象是否相等时,实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象。

我们在重新看下开头的那段代码。

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

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

但是,上面的分析还是不对,答案并非是两个 false,而是一个 true,一个 fasle。这正是因为 Integer 用到了享元模式来复用对象,才会导致这样的运行结果。当我们自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的对象的值在 -128~127 之间,会从 IntegerCache 类中直接返回,否则才会调用 new 方法创建。

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

实际上,这里的 IntegerCache 相当于上篇文章的生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。我们来看下 IntegerCache 的具体代码实现,它是 Integer 的内部类,你可以自行查看 JDK 源码。

    /**
     * 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() {}
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

为什么 IntegerCache 只缓存 -128~127 之间的整型数呢?

IntegerCache 的代码实现中,当这个类被加载的时候,缓存的享元对象会被集中一次性的创建好。我们不可能在 IntegerCache 类中预先创建好所有的整型值,这样既占用太多内存,也使得 IntegerCache 类的加载时间过长。所以,只选择缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128~127 之间的数据)。

实际上,JDK 也提供了方法让我们可以自定义缓存的最大值,有下面两种方式。

// 方法一
-Djava.lang.Integer.IntegerCache.high=255
// 方法二
-XX:AutoBoxCacheMax=255
  • 1
  • 2
  • 3
  • 4

如果你通过分析应用的 JVM 内存占用情况,发现 -128~255 之间的数据占用的内存比较多,你就可以采用上面的方式,将缓存的最大值从 127 调整至 255。不过, JDK 并没有提供最小值的设置方法。

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

除了 Integer 之外,其他包装器类型,比如 LongShortByte 等,也都利用了享元模式来缓存 -128~127 之间的数据。比如,Long 类型对应的 LongCache 享元工厂类及 valueOf() 函数代码如下所示:

	private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }

    /**
     * Returns a {@code Long} instance representing the specified
     * {@code long} value.
     * If a new {@code Long} instance is not required, this method
     * should generally be used in preference to the constructor
     * {@link #Long(long)}, as this method is likely to yield
     * significantly better space and time performance by caching
     * frequently requested values.
     *
     * Note that unlike the {@linkplain Integer#valueOf(int)
     * corresponding method} in the {@code Integer} class, this method
     * is <em>not</em> required to cache values within a particular
     * range.
     *
     * @param  l a long value.
     * @return a {@code Long} instance representing {@code l}.
     * @since  1.5
     */
    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

我们平时的开发中,对于下面这样三种创建整型对象的方式,优先使用后两种。

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

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

享元模式在 Java String 中的应用

刚刚我们讲了享元模式在 Java Integer 类中的应用,现在在看下享元模式在 Java String 类中的应用。同样,还是先来看一段代码,你觉得这段代码输出的结果是什么呢?

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

上面代码运行的结果一个是 true,一个是false。跟 Integer 的设计思路类似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的 “Hello”)。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫做 “字符串常量池”。上面代码对应的内存存储结构如下所示:

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

总结

在 Java Integer 的实现中,-128 到 127 之间的整型数会被实现创建好,缓存在 IntegerCache 类中。当我们使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。这里的 IntegerCache 类就是 享元工厂类,事先创建好的整型对象就是享元对象。

在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫做字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序运行期间,根据需要来创建和缓存字符串常量。

此外,还要补偿强调一下。

享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保持了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计,得不偿失。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/353607
推荐阅读
相关标签
  

闽ICP备14008679号