欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java自动拆装箱(Autoboxing and unboxing)学习

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

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

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

  • 什么是自动拆装箱
  • 拆装箱的实现
  • 拆装箱发生的场景
  • 关于string
  • 回首望月

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


 

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

我们都知道,java定义了8种基本类型和与之对应的8中包装器,其中6种数据类型,1种字符类型以及1种布尔类型: 
Java自动拆装箱(Autoboxing and unboxing)学习

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

1 integer i = new integer(0);

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

1 integer i = 0; //自动装箱
2 int j = i; //自动拆箱
 

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

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

1 public class maintest {
2     public static void main(string[] args) {
3         integer i = 0;
4         int j = i;
5     }
6 }

编译后:

Java自动拆装箱(Autoboxing and unboxing)学习

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

其中,拆箱方法integer.intvalue()方法很简单:

1      /**
2      * returns the value of this {@code integer} as an
3      * {@code int}.
4      */
5     public int intvalue() {
6         return value;
7     }

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

 

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

 1     /**
 2      * returns an {@code integer} instance representing the specified
 3      * {@code int} value.  if a new {@code integer} instance is not
 4      * required, this method should generally be used in preference to
 5      * the constructor {@link #integer(int)}, as this method is likely
 6      * to yield significantly better space and time performance by
 7      * 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      * @param  i an {@code int} value.
13      * @return an {@code integer} instance representing {@code i}.
14      * @since  1.5
15      */
16     public static integer valueof(int i) {
17         if (i >= integercache.low && i <= integercache.high)
18             return integercache.cache[i + (-integercache.low)];
19         return new integer(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 between
 3      * -128 and 127 (inclusive) as required by jls.
 4      *
 5      * the cache is initialized on first usage.  the size of the cache
 6      * may be controlled by the {@code -xx:autoboxcachemax=<size>} option.
 7      * during vm initialization, java.lang.integer.integercache.high property
 8      * may be set and saved in the private system properties in the
 9      * sun.misc.vm class.
10      */
11 
12     private static class integercache {
13         static final int low = -128;
14         static final int high;
15         static final integer 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         private integercache() {}
44     }

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

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

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

测试一下:

 1     public static void main(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 void main(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(int fi){
 8     }
 9     private static void bb(integer fi){
10     }

 

2.运算时拆箱

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

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

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

1 public class maintest {
2     public static void main(string[] args) {
3         integer i = 127;
4         integer j = 127;
5         i = i + j;
6     }
7 }

将上面一段代码反编译: 
Java自动拆装箱(Autoboxing and unboxing)学习

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

2.“==”判等运算

“==”运算比较特殊:

a == b

  • 当a,b都是基本类型时,直接进行比较两个变量的值是否相等
  • 当a,b都是包装器类型时,比较两个变量指向的对象所在的地址是否相等
  • 当a,b中有一个是基本类型时,会将另一个包装器类型拆箱成基本类型,然后再进行基本类型的判等比较

测试如下:

1     public static void main(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,说明比较的是对象的地址,没有发生自动拆装箱动作

看一下反编译的结果: 
Java自动拆装箱(Autoboxing and unboxing)学习

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

 

四. 关于string类型

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

 1     public static void main(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一个出来,比如锁。