关于包装类自动装箱时的缓存机制

举个简单的例子考考大家。

public class TestInteger {
    public static void main(String[] args) {

    Integer i1 = 123;
    Integer i2 = 123;
    System.out.println(i1 == i2);//输出 true

    Integer i3 = 128;
    Integer i4 = 128;
    System.out.println(i3 == i4);//输出false

    Integer i5 = new Integer(112);
    Integer i6 = new Integer(112);
    System.out.println(i5 == i6);//输出false

    }
}

i5 和 i6 不相等这个很好理解,new了两次,所以它们分别指向不同的对象(虽然对象的值相同)。

问题是,i1 和 i2 为什么相等?我们本以为既然用了包装类,大家都是对象了,虽然使用自动装箱写起来很简洁,但是 i1 和 i2 也应该指向两个不同的对象啊(又虽然对象的值相同)。而且诡异的是,只是变了一下数值而已,i3 和 i4 又不一样了。其实问题正好出在为了写起来简洁看起来方便理解起来容易我们使用了自动装箱这个操作上。

 

所以,装箱有风险,使用请谨慎。

 

而我们要想搞明白具体是怎么回事儿,就必须首先知道自动装箱是怎么实现的。我们不能只知道自动装箱的使用规则,而必须查看 Integer 类的源码。

 

首先通过 Javac TestInteger.java 命令编译源文件,然后通过 Java -verbose TestInteger 命令查看字节码内容如下:

0: bipush        123
2: invokestatic  #2                  // Method Ljava/lang/Integer;
5: astore_1
6: bipush        123
8: invokestatic  #2                  // Method Ljava/lang/Integer;
11: astore_2
12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: aload_2
17: if_acmpne     24
20: iconst_1
21: goto          25
24: iconst_0
25: invokevirtual #4                  // Method V

以上只是截取了 main 方法的前三行源码对应的字节码内容。从上面可以很清楚地看到,自动装箱其实是通过调用包装类 Integer 的静态方法 public static Integer valueOf(int i) 实现的。

然后去查看 Integer 类的该方法,源码如下:

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

很显然,它的方法体内又用到了 IntegerCache 类的一些成员变量,所以我先把 IntegerCache 的源码放到下面(为了方便查看,加了行号):

1>    private static class IntegerCache {

2>        static final int low = -128;

3>        static final int high;

4>        static final Integer cache[];

5>

6>        static {

7>            // high value may be configured by property

8>            int h = 127;

9>            String integerCacheHighPropValue =

10>                sun.misc.VM.getSavedProperty(";

11>            if (integerCacheHighPropValue != null) {

12>                try {

13>                    int i = parseInt(integerCacheHighPropValue);

14>                    i = Math.max(i, 127);

15>                    // Maximum array size is Integer.MAX_VALUE

16>                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

17>                } catch( NumberFormatException nfe) {

18>                    // If the property cannot be parsed into an int, ignore it.

19>                }

20>            }

21>            high = h;

22>

23>            cache = new Integer[(high - low) + 1];

24>            int j = low;

25>            for(int k = 0; k < cache.length; k++)

26>                cache[k] = new Integer(j++);

27>

28>            // range [-128, 127] must be interned (JLS7 5.1.7)

29>            assert IntegerCache.high >= 127;

30>        }

31>

32>        private IntegerCache() {}

33>    }

以下文章内容主要就是分析这两段源码的含义了。根据调用的先后顺序,先说 Integer 类的静态内部类 IntegerCache 再说 Integer 的静态方法 valueOf 。

 

是的,查看 Integer 类的源码的话,会发现其中包含了一个静态内部类 IntegerCache 。从名字来看,可以叫做“int整形数值包装类缓存”。它的具体作用是,当我们使用自动装箱的方式创建一个对象,并且赋值在 -128 到 127 之间(包含-128和127两个数值)的话, Java 会首先在堆内存里创建256个 Integer 对象,对应的值从 -128 到 127 。也就是说,它自动地创建了从 new Integer(-128) 一直到 new Integer(127) 的 256 个对象,然后把我们声明的变量(引用)指向那个对应的 Integer 对象。而且,在这之后再通过自动装箱的方式创建对象的话,只要取值在 -128 到 127 之间, Java 并不会再帮我们 new 新的对象,而是把变量(引用)直接指向已经创建好的 Integer 对象。所以,如果两次采用自动装箱的方式创建对象,而赋值又相同的话,这两个变量(引用)将指向同一个 Integer 对象。 Java 把这些已经提前创建好的包装类对象称为缓存。当然,如果是通过 new 的方法,而非自动装箱的方式创建包装类对象的话,即使两个对象对应的value值一样,它们也是两个完全不同的对象,所以变量(引用)是不相等的(参考文章开头的例子中的 i5 和 i6)。或者虽然采用了自动装箱的方式创建包装类对象,但是赋的值不在 -128 到 127 ,那么 Java 会直接在内存中通过 new 创建指定的包装类对象,不存在缓存不缓存的问题,所以即使两个对象对应的value值一样,它们也是两个完全不同的对象,变量(引用)是不相等的(参考文章开头的例子中的 i3 和 i4)。

 

上面先把结论抛出来了,下面逐行分析源码。

1>    private static class IntegerCache {

这是声明在 Integer 内部的静态内部类。关于静态内部类,看到个有趣的解释。有人在知乎上提问“为什么Java内部类要设计成静态和非静态两种?”下面是知乎网友的回答。

----------------------------------------------------------------------------------

根据Oracle官方的说法:

Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.

从字面上看,一个被称为静态嵌套类,一个被称为内部类。

从字面的角度解释是这样的:

什么是嵌套?嵌套就是我跟你没关系,自己可以完全独立存在,但是我就想借你的壳用一下,来隐藏一下我自己(真TM猥琐)。

什么是内部?内部就是我是你的一部分,我了解你,我知道你的全部,没有你就没有我。(所以内部类对象是以外部类对象存在为前提的)

至于具体的使用场景,我就不当翻译工了,有兴趣的直接去官网看吧。

 

作者:昭言

链接:https://www.zhihu.com/question/28197253/answer/39814613

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

----------------------------------------------------------------------------------

看到黑体字的内容,我差点笑出声,简直不能再贴切了。其实就包装类里面的缓存来说,除了 Integer 之外,其它整形的基本数据类型的包装类全都包含相应的缓存类,甚至 Boolean 也有。这个后面将会说到。

2>        static final int low = -128;

3>        static final int high;

4>        static final Integer cache[];

这里首先声明一个 int 类型的变量 low 并初始化为 -128 。接下来声明一个 int 类型的变量 high ,并没有立刻初始化。最后声明一个数组 cache[] ,准备用来存放 Integer 类型的对象。

6>        static {

7>            // high value may be configured by property

8>            int h = 127;

9>            String integerCacheHighPropValue =

10>                sun.misc.VM.getSavedProperty(";

这里开始是一个 static 代码块。整个 static 代码块的前半部分主要是对用户自定义的缓存对象数值的最大值进行约束和取值,后半部分就是创建指定数量的缓存对象。其实只有在 Integer 类的内部缓存类里面才给了用户自定义的权限,像 Short,Long 等包装类内部的缓存类,写得就比较简单了——直接限定创建创建从 new Integer(-128) 到 Integer(127) 的 256 个缓存包装类对象。这主要是因为相比于其它包装类, Integer 的使用频率最高,所以通过缓存提高执行效率的同时,还把自定义缓存对象数值的最大值的权力赋予开发者。

具体来看,首先第 7 行友情提示: high 的值可以通过 property 配置。其实看到最后,我也没明白是怎么配置的。有兴趣的可以看下面这个链接讲的配置方法,不明觉厉。

http://rednaxelafx.iteye.com/blog/680746#comments

然后第 8 行把上面声明的 h 的值设为 127 。

然后第 9 行和第 10 行通过一个看起来是系统级别的方法调用,获取到指定的缓存值,把它赋给一个 String 对象的引用。一般从文件读取到的内容(即使文本文档里写的是一个数字)或者从命令行接收到的参数都是字符串形式的。

11>            if (integerCacheHighPropValue != null) {

12>                try {

13>                    int i = parseInt(integerCacheHighPropValue);

14>                    i = Math.max(i, 127);

15>                    // Maximum array size is Integer.MAX_VALUE

16>                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

17>                } catch( NumberFormatException nfe) {

18>                    // If the property cannot be parsed into an int, ignore it.

19>                }

20>            }

这里是一个条件判断和异常捕捉代码段。

第 11 行,条件判断的内容是:假如从配置中读取到的内容不为空,则执行大括号里面的代码,否则跳出当前的 if 判断。

第13行是把字符串形式的数字转换为对应的 int 类型的数字。而且防火防盗防不怀好意,万一配置的内容并不是可以转换为 int 类型的字符串形式的数字(字符串形式的数字如 String s = "123"),而是英文字母或者汉字等,这不就出问题了吗?所以,在这行代码的外部包裹了一层 try……catch 代码块。如果真的有人不怀好意,在配置里设定的是英文字母或者汉字,结果就是 Java 不喜欢你并向你抛出一个数字格式异常(NumberFormatException),即第 17 行的代码。

接下来第 14 行,假如我们自定义的缓存值小于 127 ,Java 将自动把 i 赋值为 127, 接着按顺序往下执行 h 就会变成 127 。也就是说,最终将会创建从 new Integer(-128) 到 Integer(127) 的 256 个缓存包装类对象。所以如果我们设定的最大值比 127 小,就等于被 Java 忽略了。

第16行是说,如果我们指定的值大于 127 ,那么它还必须小于 Integer.MAX_VALUE - (-low) -1 注释里说了原因在于数组的最大长度是 Integer.MAX_VALUE (约等于 21 亿)。之所以这么设定,思考起来可能比较绕。假如我们自定义的缓存最大值正好是 Integer 类型能表示的最大值,那么从 new Integer(-128) 到 new Integer(Integer.MAX_VALUE) 将会有 Integer.MAX_VALUE + 128 + 1 个对象。其中 128 指的是从 -128 到 -1 对应的 128 个包装类对象,而 1 指的是 0 对应的包装类对象,即 new Integer(0) 。假如这样,那么 cache[] 这个数组的长度就是  Integer.MAX_VALUE + 128 + 1 。这样的话,数组的长度就不能用 int 类型来表示,而必须换用能表示更大数值的 long 类型了。这当然没什么不可以,只不过把 IntegerCache 这个缓存类里面定义的缓存最大值 h 变量的类型改成 long 然后参与计算的时候注意一下就好了,或者直接把所有的基本数据类型变量全都弄成 long 类型也可以。但是 Java 认为,一般情况下 int 类型从负21亿到正21亿的取值范围已经能够满足我们大部分的需求了。况且这是创建缓存,如果弄那么大一个数,都要惊动到 long 类型了,本来是为了提高执行效率而设计的缓存反而成了累赘,程序刚开始跑,内存里面已经有21亿个所谓的缓存对象了。。。事实上确实如此,而且我们很少会主动去修改 Java 默认的这个缓存数值。

说了一大堆,为什么要求最大值不能超过 Integer.MAX_VALUE - (-low) -1 呢?用户自定义的那个数值最终是作为缓存数组 cache[] 的最后一个数组元素 new Integer(参数) 中的那个参数的。这个参数值最小值已经固定好是 -128 ,所以假如我们随意设定一个参数值,这个数组的长度就是 参数值 + 128 + 1 。想象一下数学中的横轴坐标,其实就是 参数值加上128 的绝对值再加上0那个位置的1个对象。数组的长度想用 int(Integer)类型表示的话,最大值是 Integer.MAX_VALUE ,那么参数能表示的最大值自然就是 Integer.MAX_VALUE减去负128的绝对值再减去1了。

21>            high = h;

这一行把经过一系列判断后最终定下来的值赋给 high 变量。

经过上面的分析可以知道,这个最终值肯定是在 127 到  Integer.MAX_VALUE - (-low) -1 之间。假如我们设定的是3000,最终将会自动生成的缓存对象为 new Integer(-128) 一直到 new Integer(3000) ,一共有 3000 + 128 + 1 个对象提前存在于堆内存中。

23>            cache = new Integer[(high - low) + 1];

这里定义 cache[] 的长度,这个很容易理解。

24>            int j = low;

25>            for(int k = 0; k < cache.length; k++)

26>                cache[k] = new Integer(j++);

这是一个循环语句。要不是今天看到这个,我还一直不知道循环语句原来也可以不写大括号(前提是循环体只有一句)。首先是把 low 变量的值赋给 int 类型变量 j ,之所以这么做是因为最上面声明 low 的时候用 final 修饰了,所以 low 是不可变的。在这里把 low 的值 128 赋给 j , j 本身是默认修饰符,所以可以在循环体中进行 j++ 运算。循环很简单,就是把 cache[] 数组初始化为 new Integer(low) 到 new Integer(high) 。

28>            // range [-128, 127] must be interned (JLS7 5.1.7)

29>            assert IntegerCache.high >= 127;

30>        }

这里用了一个“断言”。假如 assert 后面的的逻辑判断结果为 true,什么也不做。假如为 false ,则抛出一个 AssertionError 。其实感觉上面的一系列判断已经能保证 IntegerCache.high 的值高于 127 了,不知道为什么这里加了这么一行代码。也许是为了调试?

32>        private IntegerCache() {}

这是该静态内部类的默认无参构造方法。

以上部分就把 IntegerCache 这个静态内部类说完了。它主要是用来自动产生从 new Integer(-128) 到 new Integer(127) 的256个 Integer 对象(如果没有经过用户自定义的话)。

①    public static Integer valueOf(int i) {

②        if (i >= IntegerCache.low && i <= IntegerCache.high)

③            return IntegerCache.cache[i + (-IntegerCache.low)];

④        return new Integer(i);

⑤    }

方便起见,我把 valueOf 方法的代码也标上了行号。

这个就很简单了。判断传入的参数 i ,如果它在 IntegerCache.low 和 IntegerCache.high 之间(默认是 -128 到 127 )的话,返回一个来自于 cache[] 的对象。这里就可以很明显地看到,它是从已经构建好的缓存数组里查找对应的 Integer 对象。而如果传入的参数不在这个范围内,则 new 一个新的 Integer 对象。

这就是开头的例子里写的,在同样采用自动装箱的方式创建 Integer 对象的情况下,

Integer i1 = 123;
Integer i2 = 123;
System.out.println(i1 == i2)

的输出结果为 true,因为 i1 和 i2 指向了同一个已经创建好的缓存对象。

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);

的输出结果为 false ,因为 128 不在 -128 到 127 的范围内,而且我们并没有修改 Java 的默认设置,所以它们执行的是上面的 ④ ,即 return new Integer(128); 执行了两次生成了两个 Integer 对象。

Integer i5 = new Integer(112);
Integer i6 = new Integer(112);
System.out.println(i5 == i6);

的输出结果为 false ,因为它们采用的是传统的 new 来创建对象,调用了各自的构造器完成了初始化。它们是两个对象,只不过对应的值相同而已。

 

如果我们打开 Long,Short,Byte,Character 的源码,会发现它们内部其实都有一个静态内部类的缓存类。不同的是它们并没有像 Integer 这样提供了用户自定义缓存最大值的功能,所以实现的代码并不难。 Boolean 里面虽然找不到缓存类,但是其实它也实现了这样的机制。毕竟 Boolean 的可取值只有 true(1)和 false(0)两个而已。而 Double 和 Float 类没有提供这样的缓存类。原因很简单,从 0 到 1 之间就有无数个小数,世界上所有的电脑的内存加起来都不够缓存的。

Boolean包装类的例子:

public class TestBoolean {
    public static void main(String[] args) {

        Boolean b1 = true;
        Boolean b2 = true;
        System.out.println(b1==b2);//输出为true

    }
}

总结一下,在我们采用自动装箱方式创建包装类对象的情况下, Java 出于提高运行效率的考虑,会自动在堆内存中为我们提前创建好从 new Integer(-128) 到 new Integer(127) 的这 256 个我们可能会频繁用到的包装类对象(前提是我们没有自定义缓存最大值)。也因此,它会在我们使用 == 进行数值判断的时候给我们带来难以预料的困扰。因此结论就是,即使是进行数值判断,也需要考虑一下是不是应该用 equals 进行判断才比较保险。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值