Byte、Short、Integer、Long内部缓存类的对比与源码分析

发布于:2023-01-22 ⋅ 阅读:(150) ⋅ 点赞:(0)

这是《水煮 JDK 源码》系列 的第7篇文章,计划撰写100篇关于JDK源码相关的文章

对于基本数据类型的包装类 ByteShortIntegerLong ,其内部实现都有一个缓存类,这个缓存类主要用于缓存固定区间的数值对象,默认为 [-128, 127],其中 Integer 的缓存区间最大值可以通过属性动态配置,而 ByteShortLong 则不能动态配置。

在平时的开发过程中,可能对于这些包装类的内部缓存没有过多的关注,如果没有阅读过相关源码,也可能完全不知道还有内部缓存类的存在,但是下面的代码所展示的或许见过。

public static void main(String[] args) {
    Integer num1 = 100;
    Integer num2 = 100;
    System.out.println("num1 与 num2 是否相等:" + (num1 == num2));

    Integer num3 = 128;
    Integer num4 = 128;
    System.out.println("num3 与 num4 是否相等:" + (num3 == num4));
}

运行上面的代码,输出结果是什么呢?了解过 Integer 内部缓存池的可能会给出如下的结果

num1 与 num2 是否相等:true
num3 与 num4 是否相等:false

但是在此处,我想说的是,这个结果不完全正确,为什么呢?因为漏掉了另外一种可能的情况,准确来说,上面的程序运行可能会出现两种结果:

  • 未改变 Integer 缓存区间最大值时,默认缓存区间为 [-128, 127],此时上面程序输出结果如下

    num1 与 num2 是否相等:true
    num3 与 num4 是否相等:false
    
  • 通过 java.lang.Integer.IntegerCache.high 属性改变了 Integer 缓存区间的最大值时,比如改变后的缓存区间为 [-128, 200],那么此时上面程序的输出结果就不一样了,如下

    num1 与 num2 是否相等:true
    num3 与 num4 是否相等:true
    

为什么会这样呢?上面的程序定义涉及到 intInteger 之间的装箱操作,而装箱操作是在编译时自动完成的,具体装箱操作调用的是 IntegervalueOf() 方法,而 valueOf() 方法的实现又与 Integer 的内部缓存类 IntegerCache 的实现有关,IntegerCache 的缓存区间最大值是可以动态配置的

那么对于 ByteShortLong 是否也是一样的呢?答案是否定的,也就是说,如果上面程序的类型改成 ByteShortLong 任意一种,其输出结果只会有一种,也就是对应 Integer 的第一种结果,下面来具体分析各个类的内部缓存类的源码实现,在分析完后,或许就有了不一样的理解和认识。

1、Byte 的内部缓存类 ByteCache

private static class ByteCache {
    private ByteCache(){}

    // 定义了一个大小为 256 的 Byte[] 数组缓存
    static final Byte cache[] = new Byte[-(-128) + 127 + 1];

    // 静态代码块用于初始化 Byte[] 数组缓存
    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Byte((byte)(i - 128));
    }
}

ByteCacheByte 的内部静态类,由于构造函数为私有的,所以不能被实例化,缓存类中只有一个成员变量 cache,它是一个大小为 256 的 Byte 数组,静态代码块初始化时,会向数组中填充 256个 Byte 对象,这些 Byte 对象对应的数值是 [-128, 127],下面再来看看 BytevalueOf() 方法的实现。

public static Byte valueOf(byte b) {
    // 偏移量为 128
    final int offset = 128;
    // 这里直接从 ByteCache 的缓存数组中获取 Byte 对象
    return ByteCache.cache[(int)b + offset];
}

public static Byte valueOf(String s, int radix)
    throws NumberFormatException {
    // 调用的还是上面的方法
    return valueOf(parseByte(s, radix));
}

public static Byte valueOf(String s) throws NumberFormatException {
    return valueOf(s, 10);
}

虽然 Byte 类提供了3个 valueOf() 方法,但是通过分析可知,最终调用的都是第一个方法,即 valueOf(byte b) 这个,由于 byte 能表示的数值区间为 [-128, 127] ,所以如果是直接通过同一数值定义多个 Byte ,这些 Byte 实例对象都是相同的,如下:

public static void main(String[] args) {
    Byte byte1 = 1;
    Byte byte2 = 1;
    Byte byte3 = 1;

    System.out.println(byte1 == byte2);
    System.out.println(byte2 == byte3);
}

输出的结果如下:

true
true

那么如果换一种方式定义 byte1byte2byte3,结果又会怎么样呢?

public static void main(String[] args) {
    Byte byte1 = new Byte("1");
    Byte byte2 = new Byte("1");
    Byte byte3 = new Byte("1");
    
    System.out.println(byte1 == byte2);
    System.out.println(byte2 == byte3);
}

运行程序输出结果如下:

false
false

因为当直接使用 new 关键字创建对象时,会直接在堆上分配新的内存空间,同时 new Byte("1") 的实现和 valueOf() 并不一样,所以 创建的 byte1byte2byte3 对象并不相同。

2、Short 的内部缓存类 ShortCache

private static class ShortCache {
    private ShortCache(){}

    // 定义了一个大小为 256 的 Short[] 数组缓存
    static final Short cache[] = new Short[-(-128) + 127 + 1];

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

ShortCache 缓存类和 ByteCache 缓存类很类似,缓存数值的大小都是 256,缓存数值区间都是 [-128, 127],只是数值的类型不一样,下面再来看看 Short 类的 valueOf() 方法实现。

public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    // 如果 s 值在 [-128, 127] 之间,则直接返回 ShortCache.cache[] 中已缓存的对象
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    
    // 不在 [-128, 127] 之间时,直接使用 new Short() 创建对象
    return new Short(s);
}

public static Short valueOf(String s) throws NumberFormatException {
    return valueOf(s, 10);
}

public static Short valueOf(String s, int radix)
    throws NumberFormatException {
    return valueOf(parseShort(s, radix));
}

Byte 类一样,Short 类也有3个 valueOf() 方法,只是 Short 类的 valueOf() 方法实现却不相同,主要由于 Short 类能表示的数值区间为 [-32768, 32767],而 ShortCache 缓存的数值区间只有 [-128, 127],所以只有当数值在 [-128, 127] 之间时,才能从缓存中复用对象,超过这些数值的,缓存中也没有。

3、Long 的内部缓存类 LongCache

private static class LongCache {
    private LongCache(){}

    // 定义了一个大小为 256 的 Long[] 数组缓存
    static final Long cache[] = new Long[-(-128) + 127 + 1];

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

LongCache 的实现和 ShortCacheByteCache 并无区别,只是类型不一样而已,下面来看看其 valeuOf() 的实现

public static Long valueOf(long l) {
    final int offset = 128;
    // 如果 l 值在 [-128, 127] 之间,则直接返回 LongCache.cache[] 中已缓存的对象
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

public static Long valueOf(String s) throws NumberFormatException {
    return Long.valueOf(parseLong(s, 10));
}

public static Long valueOf(String s, int radix) throws NumberFormatException {
    return Long.valueOf(parseLong(s, radix));
}

可以看出,Long 类的 valueOf() 方法和 Short 类的 valueOf() 方法是一样的。

4、Integer 的内部缓存类 IntegerCache

之所以把 IntegerCache 放在最后分析,是因为 IntegerCache 的实现和其他的都不太一样,具体源码如下:

private static class IntegerCache {
    // 最小值 -128
    static final int low = -128;
    // 最大值
    static final int high;
    // 缓存数组
    static final Integer cache[];

    static {
        // 最大值默认为 127,也可以通过属性字段配置,具体属性名为 java.lang.Integer.IntegerCache.high
        int h = 127;
        // 获取 java.lang.Integer.IntegerCache.high 属性值
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        // 如果属性值不为空
        if (integerCacheHighPropValue != null) {
            try {
                // 使用 parseInt() 方法将属性值转换为 int 类型
                int i = parseInt(integerCacheHighPropValue);
                // 取属性值与127两者之间的最大值,也就是说 i 的最小值其实是127
                i = Math.max(i, 127);
                // 数组的最大值为 Integer.MAX_VALUE
                // 取 i 和 Integer.MAX_VALUE - (-low) -1 两者之间最小值
                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;

        // 创建大小为 (high - low) + 1 的整型数组
        cache = new Integer[(high - low) + 1];
        int j = low;
        // 创建并缓存 Integer 对象
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        // 断言缓存最大值 high 大于等于 127
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

结合上面已经分析的 ByteCacheShortCache ,可以看出 IntegerCache 缓存类是有明显区别的,实现代码更多,从 IntegerCache 的定义可以看出,3个成员变量都是静态常量,只有一个静态代码块,没有任何其他的方法,关于静态代码块的分析已经在代码中备注了,从静态代码块中可以得出以下的几点:

  • IntegerCache 的缓存最小值 low 为 -128,与其他的类 ByteShortLong 是相同的;
  • IntegerCache 的缓存最大值 high 可以通过属性 java.lang.Integer.IntegerCache.high 进行设置,而其他的类 ByteShortLong 都是无法设置的;
  • IntegerCache 的缓存最大值 high 的最小值为127,如果通过属性设置的值比127小,则赋值为 127,也就是此时的属性值是无效的;

上面提到 IntegerCache 的缓存最大值 high 可以通过属性 java.lang.Integer.IntegerCache.high 进行设置,那么又该如何配置呢?这里以 Intellij IDEA 进行举例说明,首先找到测试类的配置,然后在 VM Options 参数一栏添加 -Djava.lang.Integer.IntegerCache.high=200 即可,如下:

::: hljs-center

image20220804102849895.png

:::

此处将 java.lang.Integer.IntegerCache.high 设置为 200,再次运行下面的程序

public static void main(String[] args) {
    Integer num1 = 100;
    Integer num2 = 100;
    System.out.println("num1 与 num2 是否相等:" + (num1 == num2));

    Integer num3 = 128;
    Integer num4 = 128;
    System.out.println("num3 与 num4 是否相等:" + (num3 == num4));
}

输出的结果如下:

num1 与 num2 是否相等:true
num3 与 num4 是否相等:true

重要提示:

在上面的示例代码中,都是直接使用 == 进行比较,主要是为了验证对象是否为同一个,但是在实际的应用中,可能需要比较的是数值是否一样,这个时候如果直接使用 == 比较,可能会出现预期之外的值,具体原因上面也分析过,所以实际开发过程中,不建议直接使用 == 对包装类进行比较,而是使用 equals 进行比较

5、Float 与 Double

分析到此处,可能有人会问,基本的数据类型中还有 floatdouble 类型,它们也有包装类 FloatDouble ,是不是它们也有内部缓存类呢?答案是否定的, FloatDouble 类中是没有内部缓存类的,也就是说不会存在对象复用,比如下面的代码:

public static void main(String[] args) {
    Float num1 = 1F;
    Float num2 = 1F;

    System.out.println(num1 == num2);

    Double num3 = 1D;
    Double num4 = 1D;

    System.out.println(num3 == num4);
}

运行程序,输出结果如下:

false
false
本文含有隐藏内容,请 开通VIP 后查看