自动拆装箱

装箱类型与基本类型

Java语言的基本思想是“一切皆对象”。因此,Java中所有的数据类型都继承自Object类。装箱类型就是这么来的:字节Byte,字符Character,短整形Short,整形Integer,长整形Long,单精度浮点小数Float,双精度浮点小数Double。还有个布尔对象Boolean。

但是,Java中创建对象是需要付出代价的。首先是空间代价。每一个Java对象都需要一个“对象头”来存储关于这个类的一些元信息,有时还需要一些“对齐字节”来填充长度。对象头本身并不大,但是相对于字节、数字这种本来就很小的对象来说,它占的比例就非常可观了。其次是时间代价,我们知道Java中的对象实际是一个引用,也就是一个指针。想要访问它实际所代表的值,我们先要访问存储指针的内存空间,然后根据指针去访问存储数值的空间。这一来二去就会浪费很多时间。

Java对象头格式

Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

Java对象头中的MarkWord字段

问题:为什么对象头各个字段的长度都是32或者64位的?

如果我们存储的是一个复杂对象,付出这些代价倒也无伤大雅。但如果我们操作的只是一个8比特的字节或者是32比特的整数,就得不偿失了。为了对这种情问题进行优化,Java不惜打破自己一切皆对象的原则,推出了基本类型。

由于装箱类型的问题都是由对象引起的,所以要解决这些问题,最根本的办法就是创建一堆不是对象的基本类型。因为不是对象,所以不需要对象头这种额外存储空间。因为不是对象,所以可以直接操作存储数值的内存空间。因为不是对象,所以基本数据类型的性能要比装箱类型好的多,使用上也更方便一些。

问题:猜猜这两个cost的量级是多少?

此外基本类型对Java中的==符号作了重载。对于一个对象来说,这个符号是用来比较两个对象的引用地址。由于装箱类型也是对象,因此,使用这个符号来比较两个专项类型,本质上比较的也是他们的引用地址。但是基本类型作了重载之后,用这个符号来比较两个基本类型,比较的就是它们的值了。

自动装箱与自动拆箱

同一个数据类型声明了两套Java类型,如果没有必要的机制来相互转换,一定会带来不必要的麻烦,甚至引发混乱。例如假如我的方法入参要求是int,但是传入的却是个integer,如果代码或者语言层面不做任何处理,是会引发编译错误的。在代码层面,我们可以调用integer上的静态方法来做强制的类型转换。在语言层面,Java为我们提供了自动装箱和自动拆箱的机制,用来在装箱类型和基本类型之间自动的快捷的进行转换。

适用场景

既然基本数据类型这么好,那干脆永远都使用基本数据类型呗。其实从我的经验上来看,的确可以说:能使用基本数据类型,就使用基本数据类型。目前来看,只有两种情况是必须要使用装箱类型的。

第一是泛型定义。大家回去可以试一下,你可以写一个List<integer>,但你写不了List<int>。这是Java在语法层面的硬性规定。实际上这个约束在Java新版本(Java9或Java10)已经被打破了。如果我们使用了新的Java版本,就可以写List<int>了。

另外当你需要表达null,并且没有任何的默认值来代替它时,就必须使用装箱类型。比如我们有一个字段Integer score。如果它的值为空代表没有做过评分,否则它的值就是对此评分的分值。这种情况下,我们就必须使用装箱类型。因为使用基本类型的话,你无法用这一个字段来表达没有评分这种情况。又如,对数据库中的nullable字段,我们一般会使用装箱类型来进行映射。极少数情况下会把数据库中的null映射为对象中的一个非null默认值。

换一个角度来说,是不是不考虑性能的情况下,装箱类型与基本类型就完全一样了呢?当然也不是这样。

一方面,由于基本类型没有null这个值,因此当Java对装箱类型进行自动拆箱时,如果装箱类型对象的值是null,那么拆箱时做会报空指针异常。另一方面就如前面所说的,用==这个符号去对比两个装箱类型时,对比的实际上是两个对象的内存地址,如果要对比它们的值,应该使用equals方法。当然使用equals方法就会面临空指针异常风险,在使用上不如基本类型那么方便。

Integer和Long的缓存

用==符号来比较两个装箱类型时,对比的实际上是两个对象的内存地址。那么,下面的代码能通过测试吗:

new Integer(127)得到的两个对象内存地址不同,这是情理之中的事情。但是为什么Integer.valueOf(127)得到的两个对象内存地址相同呢?为什么Integer.valueOf(128)的两个对象内存地址又不同了呢?

Integer.valueOf(int)的不同结果,与Integer中数值缓存有关。由于操作Integer的空间、时间性能都比较差,因此Java实现了一个IntegerCache,在其中暂存了一部分整数。当调用valueOf()方法、或者进行自动装箱时,会优先从这个缓存中拿出一个已经完成了分配内存等一系列初始化操作的Integer对象。

问题:猜猜这个缓存的数据结构是什么?

Integer缓存的默认范围是[-128, 127];这个缓存大小是可以通过配置来修改的。Long也有类似的缓存,范围也是[-128, 127],缓存大小不可变。但是Float和Double没有这种缓存。Boolean有没有缓存呢?没有。Boolean只有两个值,定义两个常量就够了。

讨论题:为什么String的对象池要用native方法,而Integer/Long的缓存直接用Java方法呢?