今天逛论坛的时候看到一个比较有趣的问题,下面的代码的执行结果是什么:
1 | public static void main(String[] args) { |
很明显这种问题涉及到自动装箱和自动拆箱,那么先看一下自动装箱和拆箱是什么时候发生的,是如何发生的。这里为了方便,结合 Integer 的源码来分析。
自动装箱,即将 char、int、float、double、long 等基本数据类型转为 Char、Integer、Float、Double、Long 等引用类型,一般来说,自动装箱/拆箱 在两种情况下会发生:赋值 和 方法调用。比如下面的代码:
1 | // 赋值时,会将基本数据类型转为引用类型。 |
那么,如何装箱和拆箱呢?在装箱的时候,会调用 valueOf() 方法,比如 Integer.valueOf()、Double.valueOf()、Boolean.valueOf() 方法等。而在拆箱的时候,会调用装箱类的 intValue()
方法。
缓存
这里有一个地方需要注意的是,在整型和长整型(int、long)装箱的时候,会将对象缓存起来,下次再用相同的值进行装箱的时候,会直接把这个缓存的对象返回。以 Integer 为例,代码如下:
1 | public static Integer valueOf(int i) { |
上面的代码是说,如果 int 的值在某个范围内的话,会创建对于的对象并缓存起来,下次再用相同的 int 值来装箱时,就直接从缓存中获取这个对象,而不用重新创建。而这个范围,最低是 -128,而最高的值,默认是 127,但是可以自定义!这个最高的值是 JVM 虚拟机中的配置文件里面定义的,只要这个值大于 127 且小于 Integer.MAX_VALUE - (-low) -1
,则可以使用定义的值作为缓存区间的上限。
还有一点,缓存其实使用的一个数组来存储,并没有缓存在常量池中。相比起来,Long 里面对于缓存的处理就简单粗暴了:
1 | private static class LongCache { |
比较
那么,基本数据类型和对应装箱类型是怎么比较的呢?这个其实看一下编译后的 .class 文件就知道了,比如
1 | public static void main(String[] args) { |
编译为 .class 后就是:
1 | public static void main(String[] args) { |
可以看见,基本数据类型和装箱类的比较,其实就是拆箱比值。
那么开头提到的问题就解决了:
- 第一个比较,因为
new Integer()
创建了一个新的对象,所以和 b 不是同一个对象,因此为 false。 - 第二个比较,因为基本数据类型和装箱类型的比较只比较值,因此两个相等,答案为 ture。
- 第三个比较,在装箱的时候,100 对应的装箱类是会被缓存的,因此 f2 和 f1 是同一个对象。答案是 true。
- 第四个比较,由于 150 大于默认的 127,因此装箱类并不会被缓存,f3 和 f4 不是同一个对象,答案是 false。
重载与自动装箱
自动装箱和自动拆箱会产生一个问题,在调用方法的时候可以自动装箱,那么重载的情况呢?比如下面的代码:
1 | public static void main(String[] args) { |
实际运行一下就会知道,在有重载情况发生的时候,是不会发生自动装箱的。
自动装箱的弊端
有如下的代码:
1 | public static void main(String[] args) { |
运行的结果是正确的,但是会消耗大量内存,因为 sum 被声明称 Long 而不是 long,这意味着程序创建了大约 $2^{31}$ 个多余的 Long 对象。因此,要优先使用基本类型而不是装箱类型,要当心无意识的自动装箱。