java什么是自动拆装箱_Java自动拆装箱(Autoboxing and unboxing)学习

在学习并发的过程中,用“Boolean bool = true”的自动装箱方式初始化了两个对象锁去锁两块代码,结果运行的时候出现了竞争等待,调试了一下发现两个锁变量指向的是同一个对象,由此可见我对自动拆装箱的机制想的太简单了,查了一下,发现这个机制还挺细节,那就记录一下:

本文主要有以下几个方面:

什么是自动拆装箱

拆装箱的实现

拆装箱发生的场景

关于String

回首望月

尝试的第一篇博客,不当之处,求轻喷!

一. 什么是自动拆箱与装箱

我们都知道,Java定义了8种基本类型和与之对应的8中包装器,其中6种数据类型,1种字符类型以及1种布尔类型:2be9121e4036b70bb6871eec40c758e0.png

在Java5之前,定义生成一个Integer包装器类型的对象,只能通过以下方式:

1 Integer i = new Integer(0);

Java5支持了基本类型和对应的包装类型之前的自动转换机制,即自动拆箱(包装器类型转换成基本类型)与装箱(基本类型封装成包装器类型)。于是,就有了以下两行代码:

1 Integer i = 0; //自动装箱

2 int j = i; //自动拆箱

二. 自动拆装箱的实现(int-Integer为例)

我们将下面自动拆装箱的代码反编译一下,拆装箱的动作就一目了然。

1 public classMainTest {2 public static voidmain(String[] args) {3 Integer i = 0;4 int j =i;5 }6 }

编译后:

011afb1b467d9ca3ec3b99b97435b3da.png

通过反编译的结果看,在"Integer i = 0"自动装箱的过程中,调用了Integer.valueOf(int i)方法;在"int j = i;"的自动装箱的过程中,调用了Integer.intValue()方法。

其中,拆箱方法Integer.intValue()方法很简单:

1 /**

2 * Returns the value of this {@codeInteger} as an3 * {@codeint}.4 */

5 public intintValue() {6 returnvalue;7 }

只是返回了当前对象的value值,没什么好说的。

但是装箱方法Integer.valueOf(int i)就有细节了,一起看下:

1 /**

2 * Returns an {@codeInteger} instance representing the specified3 * {@codeint} value. If a new {@codeInteger} instance is not4 * required, this method should generally be used in preference to5 * the constructor {@link#Integer(int)}, as this method is likely6 * to yield significantly better space and time performance by7 * caching frequently requested values.8 *9 * This method will always cache values in the range -128 to 127,10 * inclusive, and may cache other values outside of this range.11 *12 *@parami an {@codeint} value.13 *@returnan {@codeInteger} instance representing {@codei}.14 *@since1.515 */

16 public static Integer valueOf(inti) {17 if (i >= IntegerCache.low && i <=IntegerCache.high)18 return IntegerCache.cache[i + (-IntegerCache.low)];19 return newInteger(i);20 }

这边的源码比预想的多了一个细节操作,值落在[IntegerCache.low, IntegerCache.high]区间上时,是直接从一个Integer类型的缓存数组IntegerCache.cache中取一个对象返回出去,值不在这个区间时才new一个新对象返回。看一下IntegerCache的实现,它是Integer类的一个私有静态内部类:

1 /**

2 * Cache to support the object identity semantics of autoboxing for values between3 * -128 and 127 (inclusive) as required by JLS.4 *5 * The cache is initialized on first usage. The size of the cache6 * may be controlled by the {@code-XX:AutoBoxCacheMax=} option.7 * During VM initialization, java.lang.Integer.IntegerCache.high property8 * may be set and saved in the private system properties in the9 * sun.misc.VM class.10 */

11

12 private static classIntegerCache {13 static final int low = -128;14 static final inthigh;15 static finalInteger cache[];16

17 static{18 //high value may be configured by property

19 int h = 127;20 String integerCacheHighPropValue =

21 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");22 if (integerCacheHighPropValue != null) {23 try{24 int i =parseInt(integerCacheHighPropValue);25 i = Math.max(i, 127);26 //Maximum array size is Integer.MAX_VALUE

27 h = Math.min(i, Integer.MAX_VALUE - (-low) -1);28 } catch( NumberFormatException nfe) {29 //If the property cannot be parsed into an int, ignore it.

30 }31 }32 high =h;33

34 cache = new Integer[(high - low) + 1];35 int j =low;36 for(int k = 0; k < cache.length; k++)37 cache[k] = new Integer(j++);38

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

40 assert IntegerCache.high >= 127;41 }42

43 privateIntegerCache() {}44 }

IntegerCache中有3个final类型的变量:

low:-128(一个字节能够表示的最小值);high:127(一个字节能够表示的最大值),JVM中设置的属性值(通过-XX:AutoBoxCacheMax=设置)二者取大值,再和Integer.MAX_VALUE取小值;cache:在静态块中初始化为由区间[low, high]上的所有整数组成的升序数组。

综上,Java在虚拟机堆内存中维持了一个缓存池,在装箱的过程中,如果发现目标包装器对象在缓存池中已经存在,就直接取缓存池中的,否则就新建一个对象。

测试一下:

1 public static voidmain(String[] args) {2 Integer i = 127;3 Integer j = 127;4 System.out.println(System.identityHashCode(i)); //本地输出i的地址:1173230247

5 System.out.println(System.identityHashCode(j)); //本地输出j的地址:1173230247

6

7 Integer m = 128;8 Integer n = 128;9 System.out.println(System.identityHashCode(m)); //本地输出m的地址:856419764

10 System.out.println(System.identityHashCode(n)); //本地输出n的地址:621009875

11 }

由测试结果来看,值为127时,两次装箱返回的是同一个对象,值为128时,两次装箱返回的是不同的对象。

因为小数的区间取值无限,所以float->Float,double->Double两种类型装箱机制没有缓存机制,其他5中基本类型的封装机制也是类似int->Integer的装箱套路,不过缓存的边界不可改变:

基本类型包装器类型缓存区间缓存是否可变

byte

Byte

[-128, 127]

不可变

short

Short

[-128, 127]

不可变

int

Integer

[-128, 127]

上限可设置

long

Long

[-128, 127]

不可变

float

Float

--

--

double

Double

--

--

char

Character

[0, 127]

不可变

boolean

Boolean

{true, false}

不可变

因为基本类型对应的包装器都是不可变类,它们的缓存区间一旦初始化,里面的值就无法再改变,所以在JVM运行过程中,所有的基本类型包装器的缓存池都是不变的。

三. 拆装箱发生的场景

1.定义变量和方法参数传递:

这里的拆装箱是指开发者通过编写代码控制的拆装箱,比较明显:

1 public static voidmain(String[] args) {2 Integer i = 0; //装箱

3 int j = i; //拆箱

4 aa(i); //拆箱,传值时发生了:int fi = i;

5 bb(j); //装箱,传值时发生了:Integer fi = j;

6 }7 private static void aa(intfi){8 }9 private static voidbb(Integer fi){10 }

2.运算时拆箱

我们都知道,当一个变量的定义类型不是基本类型,其实变量的值是对象的在虚拟机中的地址,当用初始化后的包装器类型变量进行运算时,会发生什么呢?

1.“+,-,*,/ ...”等运算时拆箱

当用包装器类型的数据进行运算时,JAVA会先执行拆箱操作,然后进行运算。

1 public classMainTest {2 public static voidmain(String[] args) {3 Integer i = 127;4 Integer j = 127;5 i = i +j;6 }7 }

将上面一段代码反编译:ebbff80d82ed03fd05ac688afcbba2ac.png

发现,除了在分别源码的3,4行进行了装箱操作后,在执行add操作之前,有两次拆箱操作,add之后,又把结果装箱赋值给变量i。

2.“==”判等运算

“==”运算比较特殊:

A == B

当A,B都是基本类型时,直接进行比较两个变量的值是否相等

当A,B都是包装器类型时,比较两个变量指向的对象所在的地址是否相等

当A,B中有一个是基本类型时,会将另一个包装器类型拆箱成基本类型,然后再进行基本类型的判等比较

测试如下:

1 public static voidmain(String[] args) {2 int m = 128;3 int n = 128;4 Integer i = 128;5 Integer j = 128;6 System.out.println(m == n); //输出:true

7 System.out.println(m == i); //输出:true

8 System.out.println(i == j); //输出:false

9 }

前文已经说了,JVM没有设置Integer类型的缓存上限的时候,128不在缓存池内,所以两次封装后的对象是不同的对象。在此基础上:

第6行输出true:如果比较的是装箱后的对象地址,结果肯定是false,实际结果是true,说明比较的是基本类型的值,没有发生自动拆装箱动作

第7行输出true:如果比较的是装箱后的对象地址,结果肯定是false,实际结果是true,说明比较的是基本类型的值,那么包装器类型的变量肯定进行了自动拆箱动作

第8行输出false:如果比较的是拆箱后的基本类型的值,结果肯定是true,实际结果是false,说明比较的是对象的地址,没有发生自动拆装箱动作

看一下反编译的结果:fe749fe7730435322e817c21c8d61b77.png

对应源码中除了第4、5行出现了自动装箱动作,就只有在第7行发生了自动拆箱动作。

四. 关于String类型

String类型没有对应的基本类型,所以没有自动拆装箱的机制,之所以在这里提一下,是因为String的初始化过程和自动装箱的过程很像。

1 public static voidmain(String[] args) {2 String s1 = "hello";3 String s2 = "hello";4 String s3 = new String("hello");5 String s4 = new String("hello");6 System.out.println(System.identityHashCode(s1)); //输出s1地址:1173230247

7 System.out.println(System.identityHashCode(s2)); //输出s2地址:1173230247

8 System.out.println(System.identityHashCode(s3)); //输出s3地址:856419764

9 System.out.println(System.identityHashCode(s4)); //输出s5地址:621009875

10 }

从上面的输出结果可以看出,两个直接用字符串赋值的变量s1,s2指向的是同一个对象,而new String()生成对象赋值的变量s3,s4则是不同的对象。其实,JVM中存在一个字符串缓存池,当直接使用字符串初始化变量的时候,JAVA会先到字符串缓存池中查看有没有相同值的String对象,如果有,直接返回缓存池中的对象;如果没有,就new出一个新的对象存入缓存池,再返回这个对象。而String的不可变性质则能保证在对象共享的过程中不会出现线程安全问题。与基本类型的缓存池相比,String类型的缓存池在运行时是动态变化的。

五. 回首望月

回到最开始我碰到的问题,当我用“Boolean bool = true”的自动装箱方式定义变量的时候,这两个变量其实指向的都是Boolean类型的缓存池中的那个值为true的对象,所以用他们当做同步锁,其实是用的同一把锁,自然会出现竞争等待。

经验:当我们使用自动装箱机制初始化变量的时候,就相当于告诉JAVA这里需要一个对象,而不是告诉JAVA这里需要一个新的对象。当我们需要一个新的对象的时候,为了保险起见,自己new一个出来,比如锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值