Java自动拆装箱机制

概述


Java自动拆装箱机制从Java 1.5开始引入,目的是将原始类型值转换成对应的对象。
在自动拆装箱机制引入前,由于Java不允许直接向集合类(Connections)中放入原始类型值,只能接收对象。通常这种情况下的做法是,将原始类型值转换为对象,然后将这些对象放入集合中。为了让代码更为简洁,Java 1.5引入了自动拆装箱机制,但该机制并非完美,如果有些细节问题不注意,会引起难以察觉的bug。有很多公司笔试题也爱考相关的问题,博主最近在准备应聘,所以就来好好谈谈这个自动拆装机制。

什么是自动装箱和拆箱


自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。
原始类型:byte,short,char,int,long,float,double,boolean
对应的封装类分别为:Byte,Short,Character,Integer,Long,Float,Double,Boolean。

什么时候发生自动拆装箱


赋值时

这是最常见的一种情况。

1
2
3
4
5
6
7
//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()

//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion

方法调用时

方法调用时,传入的参数编译器会帮我们转换,个人认为传参的过程就是赋值的过程,所以这种情况和赋值时发生的自动拆装箱本质上应该是相同的。

1
2
3
4
5
6
7
8
public static Integer show(Integer iParam) {
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

自动装箱的弊端


自动装箱的过程会创建对象,如果在循环中不断的执行自动装箱,就会创建多余的对象,影响程序性能。如下面例子所示:

1
2
3
4
Integer sum = 0;
for(int i=1000; i<5000; i++) {
sum+=i;
}

上面的代码 sum+=i 可以看成 sum = sum + i,但是 + 这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成 Integer对象。其内部变化如下:

1
2
sum = sum.intValue() + i;
Integer sum = new Integer(result);

在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点。

重载与自动装箱


当重载遇上自动装箱时,情况会比较有些复杂,可能会让人产生有些困惑。在1.5之前,value(int)和value(Integer)是完全不相同的方法,开发者不会因为传入是int还是Integer调用哪个方法困惑,但是由于自动装箱和拆箱的引入,处理重载方法时稍微有点复杂。一个典型的例子就是ArrayList的remove方法,它有remove(index)和remove(Object)两种重载,我们可能会有一点小小的困惑,其实这种困惑是可以验证并解开的,通过下面的例子我们可以看到,当出现这种情况时,不会发生自动装箱操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void test(int num) {
System.out.println("method with primitive argument");

}

public void test(Integer num) {
System.out.println("method with wrapper argument");

}

//calling overloaded method
AutoboxingTest autoTest = new AutoboxingTest();
int value = 3;
autoTest.test(value); //no autoboxing
Integer iValue = value;
autoTest.test(iValue); //no autoboxing

Output:
method with primitive argument
method with wrapper argument

要注意的事项


对象相等比较

这是一个比较容易出错的地方,”==“可以用于原始值进行比较,也可以用于对象进行比较,当用于对象与对象之间比较时,比较的不是对象代表的值,而是检查两个对象是否是同一对象,这个比较过程中没有自动装箱发生。进行对象值比较不应该使用”==“,而应该使用对象对应的equals方法。看一个能说明问题的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AutoboxingTest {

public static void main(String args[]) {

// Example 1: == comparison pure primitive – no autoboxing
int i1 = 1;
int i2 = 1;
System.out.println("i1==i2 : " + (i1 == i2)); // true

// Example 2: equality operator mixing object and primitive
Integer num1 = 1; // autoboxing
int num2 = 1;
System.out.println("num1 == num2 : " + (num1 == num2)); // true

// Example 3: special case - arises due to autoboxing in Java
Integer obj1 = 1; // autoboxing will call Integer.valueOf()
Integer obj2 = 1; // same call to Integer.valueOf() will return same
// cached Object

System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true

// Example 4: equality operator - pure object comparison
Integer one = new Integer(1); // no autoboxing
Integer anotherOne = new Integer(1);
System.out.println("one == anotherOne : " + (one == anotherOne)); // false

}

}

Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

值得注意的是第三个小例子,这是一种极端情况。obj1和obj2的初始化都发生了自动装箱操作。但是处于节省内存的考虑,JVM会缓存-128到127的Integer对象。因为obj1和obj2实际上是同一个对象。所以使用”==“比较返回true。

容易混乱的对象和原始数据值

另一个需要避免的问题就是混乱使用对象和原始数据值,一个具体的例子就是当我们在一个原始数据值与一个对象进行比较时,如果这个对象没有进行初始化或者为Null,在自动拆箱过程中obj.xxxValue,会抛出NullPointerException,如下面的代码:

1
2
3
4
5
6
private static Integer count;

//NullPointerException on unboxing
if( count <= 0){
System.out.println("Count is not started yet");
}

缓存的对象

这个问题就是我们上面提到的极端情况,在Java中,会对-128到127的Integer对象进行缓存,当创建新的Integer对象时,如果符合这个这个范围,并且已有存在的相同值的对象,则返回这个对象,否则创建新的Integer对象。

在Java中另一个节省内存的例子就是字符串常量池,感兴趣的同学可以了解一下。

生成无用对象增加GC压力

因为自动装箱会隐式地创建对象,像前面提到的那样,如果在一个循环体中,会创建无用的中间对象,这样会增加GC(Garbage Collection,垃圾收集)压力,拉低程序的性能。所以在写循环时一定要注意代码,避免引入不必要的自动装箱操作。

总结


自动拆装箱机制的引入极大的方便了程序的编写,虽然同是也增加了一些问题,不过只要注意上述事项,就可以很好的使用自动拆装箱机制以及应对相关问题。

参考文章:javarevisited

评论