参考文章:深入剖析Java中的装箱和拆箱
一、什么是装箱/拆箱
在讲之前,得先提一下为什么两个概念:基本数据类型及其包装类,我们都知道Java
是一种面向对象的语言,但是Java
中的基本数据类型是不面向对象的,这时在使用中便会存在诸多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的包装类(Wrapper Class
),他们之间的对应关系如下表:
基本类型 | 包装类 |
---|---|
boolean |
Boolean |
char |
Character |
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
void |
Void |
弄清楚了这个概念之后,我们回到正题,在Java SE5之前,如果要生成一个数值为10的Integer对象,只需要这样就可以了:
Integer i = new Integer(10);
而从Java SE5开始就提供了自动装箱的特性,如果生成一个数值为10的Integer对象,只需这样就可以了:
Integer i = 10;
这个过程中会自动根据数值创建对应的Integer对象,这就是装箱。那么什么是拆箱呢,顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:
Integer i = 10 //装箱
int n = i; //拆箱
简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
二、拆箱与装箱是如何实现的?
以下面这段代码为例:
public class Main {
public static void main(String[] args){
Integer i = 10;
int n = i;
}
}
利用IDEA
对其进行反编译:
从我圈出来的地方可以看出,在装箱的时候调用的是
Integer
的valueOf(int)
方法。而在拆箱的时候自动调用的是Integer
的intValue
方法。
其他的也类似,如Double
,Character
,在这里就不展示截图了。
由此可得出结论:
装箱过程是通过调用包装器的valueOf
方法实现的,二拆箱是通过调用包装器的xxxValue
方法实现的。(xxx
代表对应的基本数据类型)
三、总结一些常见的面试题
虽然到这里你已经明白的装箱和拆箱的概念,但是如果碰到了相关的考题却不一定能答得上来,下面就来列举一些常见的相关考题:
- 下面这段代码的输出结果是什么?
public class Main {
public static void main(String[] args){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
先直接上结果吧:
分析:
首先,在创建Integer
对象的时候,设计到一个自动装箱的过程,从上面的分析我们得知这其中涉及到Integer
的valueOf
方法,那么我们就从这里入手:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//Integer中的静态内部类
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 =
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 Integer.IntegerCache.high >= 127;
}
private IntegerCache() {}
}
从源码中我们可以得知,在通过valueOf
方法创建Integer
对象的时候,如果数值在[-128
,127
]之间,便返回指向IntegerCache.cache
中已经存在的对象的引用,否则创建一个新的Integer
对象。
所以上面例子中i1
和i2
指向的是同一个对象,而i3
和i4
则分别指向不同的对象。
- 再来看看下面找个例子:
public class Main {
public static void main(String[] args){
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
是不是在觉得这跟上面那个例子一样一样的?先来看看答案:
why
?先来看看Double
的valueOf
是怎么实现的:
public static Double valueOf(double d) {
return new Double(d);
}
这下明白结果是怎么来的了吧?还不懂?其实很简单,整数100
与101
是连续的,但是对于浮点数,100.1
与100.2
之间有多少个浮点数?无数个!所以这里无法像Integer
那样做一个缓存。
到了这里,已经明白了原理,就可以做一个总结了:
-
Integer
、Short
、Byte
、Character
、Long
这几个类的valueOf
方法的实现是类似的,Double
、Float
的valueOf
方法的实现是类似的。 -
xxxCache
存在的意义:缓存缓存,当然是提高效率,避免多次重复创建相同对象了!(当然可能还有其他原因只是我不知道罢了 = =)
- 继续上例子:
public class Main {
public static void main(String[] args) {
Boolean b1 = false;
Boolean b2 = false;
Boolean b3 = true;
Boolean b4 = true;
System.out.println(b1 == b2);
System.out.println(b3 == b4);
}
}
猛地发现前面只总结7种数据类型,还有最优一种。老规矩先上答案:
源码:
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
ok,我们可以看到Boolean
内部定义了两个静态常量,可以理解成提前做了缓存吧,只不过bool
类型只有true
和false
,所以没必要像Integer
那样做一个cache
了。
-
Integer i = new Integer(xxx)
和Integer i = xxx
两种方式的区别
这个题目可以从多个角度切入,但是自动装/拆箱的要点一定要答上,例如:- 第一种方式不会触发自动装箱的过程;而第二种方式会触发;
- 在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般情况下要优于第一种情况(注意这并不是绝对的)。
-
最后再来一个大练习:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
System.out.println(g.equals(a + h));
}
虽然上面分别讲了下这些包装类的valueOf方法的实现,但是这里可能还是会犯迷糊,下面先提供两点提示:
- 当
==
运算符的两个操作数都是包装类的引用时比较的是它们是否指向同一个引用,而如果其中有一个操作数是表达式(包含算术运算)则比较的是数值(即触发自动拆箱的过程)。 - 对于包装类,
equals
方法并不会进行类型转换。
下面是运行结果:
分析:
首先前两个结果相信没有什么异议。第三个由于a+b
是一个算术运算,因此会触发自动拆箱,最终比较的是数值,所以结果为true
,g == (a + b)
同理。
再来说下第四个,首先a+b
最终得到的是一个Integer
类型的值,然后再执行equals
方法,后面两个同理。
四、总结
- 装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
-
装箱过程是通过调用包装器的
valueOf
方法实现的,二拆箱是通过调用包装器的xxxValue
方法实现的。(xxx
代表对应的基本数据类型) -
Integer
、Short
、Byte
、Character
、Long
这几个类的valueOf
方法的实现是类似的,Double
、Float
的valueOf
方法的实现是类似的。Boolean
定义了两个静态常量当做缓存。 -
xxxCache
存在的意义:提高效率,避免多次重复创建相同对象了。 - 当
==
运算符的两个操作数都是包装类的引用时比较的是它们是否指向同一个引用,而如果其中有一个操作数是表达式(包含算术运算)则比较的是数值(即触发自动拆箱的过程)。 - 对于包装类,
equals
方法并不会进行类型转换。
网友评论