简介 Java 自动装箱和拆箱机制

Java 自动装箱和拆箱机制的理解

一、概念

Java 有 8 种原始数据类型,为了充分体现 Everything is Object 的理念,Java 为各个原始数据类型又都引入了对应的 Object 类 (一般称之为 Wrapper Class),下面是它们的对应表:

WrapperClassesTables

如果你想在原始数据类型和其对应的 Object 类型之间相互转换,那么可以像下面这样编写程序:

1
2
Integer age = Integer.valueOf( 22 );
int ageInt = age.intValue();

然而这两个之间互相转换还是太繁琐了,为了进一步方便开发者开发,Java 在 1.5 版本引入了一颗语法糖,其能够让编译器来实现原始数据类型和对应的 Object 类之间的自动转换。

其中,Java 编译器自动将原始数据类型转为它们所对应的过程称之为自动装箱;其相反过程称之为自动拆箱

autoboxing-and-unboxing-in-java

基于此机制,上面程序可以重写成这样:

1
2
Integer age = 22;
int ageInt = age;

下面是一个更复杂的例子:

1
2
3
4
5
6
List<Integer> list = Arrays.asList(1, 2, 3, 4);

int sum = 0;
for (int i : list) {
sum += i;
}

上面代码在编译器的帮助下,最终生成的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
List list = Arrays.asList(new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4)
});

int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext()) {
int i = ( (Integer) localIterator.next() ).intValue();
sum += i;
}

二、性能影响

摘自Autoboxing对于性能部分的说明:

It is not appropriate to use autoboxing and unboxing for scientific computing, or other performance-sensitive numerical code. An Integer is not a substitute for an int; autoboxing and unboxing blur the distinction between primitive types and reference types, but they do not eliminate it.

也就是说这种机制肯定对于性能来说是有损耗的。当你有意或者无意这样写的时候,Java 编译器最终都会为你创建相应的对象的:

1
2
3
4
5
6
7
8
9
10
// Integer.valueOf(1);
// Integer.valueOf(2);
// Integer.valueOf(3);
// Integer.valueOf(4);
//
List<Integer> list = Arrays.asList(1, 2, 3, 4);

Map<Integer, String> map = new HashMap<>();
// Integer.valueOf( 5 );
map.get( (Integer) 5 );

也许当向集合中添加元素,创建对象的行为,你尚可以理解。但是使用强制转换也要创建一个对象,这种着实让人背后一热:

1
2
// 我们不经意间创建了一个对象
map.get( (Integer) 5 );

假设相同的语句散布在我们程序的各个地方,就意味着同样的对象被重复的创建,那确实是一笔不小的性能开销啊!

三、性能优化

Oracle 官方通过为各个 Wrapper 类引入了一个缓存机制,来解决这一性能问题。

在 Java Integer 类的源码内部有一个称之为 IntegerCache 的类,以下是它在 JDK 1.8 版本中的源码:

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
36
37
38
39
40
41
42
43
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

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

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

我们可以看到它在 JVM 初始化的时候,创建了一个长度为 (127 - (-128)) + 1 的 cache 数组:

1
cache = new Integer[(high - low) + 1];

当使用自动装箱机制的时候,其会首先从缓存中查找对应的对象,如果有,就不会在创建相应的对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Integer.java JDK 1.8
public final class Integer extends Number implements Comparable<Integer> {

private static class IntegerCache {

// ...

static final Integer cache[];

// ...

}

public static Integer valueOf(int i) {
// 首先从缓存中查找
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];

return new Integer(i);
}

}

通过引入这一缓存机制,能够改善自动装箱机制带来的性能损耗。对于其他的数据类型,其 Wrapper 类也都有相应的缓存机制。

四、对象比较

相同 Wrapper 类型比较:

1
2
3
Integer A = 100;
Integer B = 100;
boolean result = ( A == B );

上述因为 A 和 B 都是对象,所以通过 == 判断等于的时候,判断的是 A 和 B 这两个引用指向的是否是相同的对象:

autoboxing_integer_100

Integer_Compare_100

而我们现在已经知道由于 IntegerCache 缓存的存在,100 位于 -128 到 127 之间,所以这两个引用指向的是相同的对象,因此结果为 true。

相同 Wrapper 类型比较 (值大于 127):

1
2
3
Integer A = 200;
Integer B = 200;
boolean result = ( A == B );

autoboxing_integer_200

Integer_Compare_200

由于 200 位于 -128 到 127 之外,所以不在缓存的范围以内,因此上述 A 和 B 引用指向的都是两个不同的对象,因此结果为 false。

正确的对比方法应该是使用 equals 方法来判断两个对象存储的值是否是相同的:

1
boolean result = A.equals( B );

Integer 类的 equals 源码如下:

1
2
3
4
5
6
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

可见,equals 方法会使用原始属于类型进行比较。

一个 Wrapper 类型比较一个原始类型:

1
2
3
4
Integer A = 123;
int a = 123;
boolean cmpResult1 = ( A == a );
boolean cmpResult2 = ( a == A );

当 == 号两侧有一个是 Wrapper 类型的话,那么 Wrapper 会首先执行拆箱机制,以便与原始类型进行比较:

Integer_Autounboxing_123

由于这个地方对比的是原始数据类型,因此没有 IntegerCache 的影响,因此比较结果与数值大小是无关的,两个值都为 true。

当一个 Wrapper 类型和一个原始类型,执行算术操作、比较操作的时候,自动拆箱机制就会发生作用。例如下面的算术加操作:

1
2
3
4
5
Integer a = 10;
// 1. Unboxing a to int (a.intValue())
// 2. calculate a + 10
// 3. Boxing 20 to Integer
a = a + 10;

再例如下面的比较操作:

1
2
3
4
Integer a = 20;
// 1. Unboxing a to int
// 2. Compare a (20) and 10
boolean cmpResult = ( a > 10 );

相同值不同 Wrapper 类型:

对不起,不兼容类型不能作比较:

Integer_Long_Cmp

五、参考

推荐文章