1 前言
java是面向对象的语言,但是其也包含了8种基本数据类型,这8种数据类型不支持面向对象的编程机制,即不具备对象的特性,可以说是java语言早期设计的一种缺陷,为了解决8种基本数据类型的变量不能当成Object类型变量使用的问题,java提出了包装类(Wrapper class)的概念。本节主要了解:
- 基本类型与包装类型之间的转换
- 什么是装箱?什么是拆箱?
- 装箱和拆箱是如何实现的
- 装箱过程中的优化
2 正文
2.1 基本类型与包装类型之间的转换
下表格展示了8种基本类型对应的包装类型
基本数据类型 | 包装类 | 基本数据类型 | 包装类 |
---|---|---|---|
byte | Byte | char | Character |
short | Short | float | Float |
int | Integer | Double | double |
long | Long | boolean | Boolean |
把基本类型转换成包装类型可以通过包装类的构造器来实现,比如
int a = 5;
// 通过构造器把基本整型变量包装成包装类型
Integer aObj = new Integer(a);
boolean b = true;
// 通过构造器把基本布尔变量包装成包装类型
Boolean bObj = new Boolean(b);
Note:
- 如果传入构造器的类型不对,则会抛出java.lang.NumberFormatException
- 当试图使用一个字符来创建Boolean对象时,比如传入的是true(可以忽略大小写),则会返回true对应的Boolean对象;如果传入其他字符(非false字符时也一样),则会创建false对应的Boolean对象,从源码中可以看出:
public Boolean(String s) {
this(parseBoolean(s));
}
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
当然,如果想将包装类型转换回基本类型,使用xxxValue()实例方法即可,比如
Integer aObj = new Integer(5);
// 取出aObj对象中的int变量
int a = aObj.intValue();
Boolean bObj = new Boolean("true");
// 取出bObj对象中的boolean变量
boolean b = bObj.booleanValue();
2.2 什么是装箱?什么是拆箱?
通过2.1可以了解到如何获取一个包装类。但是那样我觉得非常麻烦,因为基本类型的数值已经确定了,为什么还要通过构造器的方式来获取一个新的对象,不能直接通过赋值的方式吗?于是,jdk1.5就出了装箱和拆箱这两种概念。
// 装箱
Integer a = 5;
// 拆箱
int b = a;
装箱:通过指定的数值自动创建一个相对应的包装类型
拆箱:自动将包装类型转换为基本类型
2.3 装箱和拆箱是如何实现的
那么装箱和拆箱是如何实现的呢,使用javap -c Test指令对2.2的代码进行反编译,可以看到:
iconst_5:把常量5从常量池中取出来并压入栈中
invokestatic:调用Integer的static方法valueOf
astore_1:从栈中弹出对象引用,然后将其存到位置为1的局部变量中,即Integer a = 5
aload_1:将位置为1的对象引用局部变量压入栈,即将a引用压入栈中
invokevirtual:调用实例方法intValue
istore_2:从栈中弹出int类型值,然后将其存到位置为2的局部变量中
位置1,2是针对代码块来说的
Integer a = 5;//1
int b = a;//2
装箱的时候调用的是Integer.valueOf(int)方法,而在拆箱的时候调用的是Integer的intValue方法。并且可以看到,装箱的过程中,返回的是一个对象,这里产生了内存的消耗,会影响性能。
综上,装箱时调用的是包装器的valueOf方法实现的,而拆箱时调用的是xxxValue方法实现的
2.4 装箱过程中的优化
关于装箱或者拆箱,在面试时可能会遇到这样一道题:
Integer ina = 2;
Integer inb = 2;
// 输出true
System.out.println(ina == inb);
Integer biga = 128;
Integer bigb = 128;
// 输出false
System.out.println(biga == bigb);
为什么ina和inb经过装箱后,两者表明指向同一对象;而biga和bigb经过装箱后,指向的不是同一个对象呢?来看一下装箱的源码便知道了:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
当传入的数值i在区间[low,high]内,便会从IntegerCache中获取;否则直接new一个新的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 =
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() {}
}
可以看出来,IntegerCache中定义了一个Integer cache[]
数组,当满足-128<=i<=127
,就会放入数组中。在上述例子中,ina和inb在自动装箱的过程中调用了valueOf
方法,由于数值在[-128,127]的范围内,所以会引用同一个对象;而对于biga和bigb来说,自动装箱的过程中同样会调用valueOf
方法,由于不在范围内,所以创建了新的对象。
下面再来看一段程序:
Double aDouble = 2.0d;
Double bDouble = 2.0d;
System.out.println(aDouble == bDouble);
结果输出的是false,查看源码Double.valueOf,发现并没有进行缓存。这是因为浮点数在一个区间内的数是无穷多个的,所以没有办法进行缓存。
再来看一段程序:
Boolean aTrue = true;
Boolean bTrue = true;
System.out.println(aTrue == bTrue);
结果输出的是true,来看一下源码Boolean.valueOf:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
发现布尔变量b直接与变量TRUE
和FALSE
关联在一起,再来看下这两个变量:
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
这两个变量用static final修饰,即为全局变量,所有的Boolean实例共享。所以无论是什么Boolean(true或者false类型一致)对象,都会有共同的地址,用==判断当然为真
总结一下,哪些包装类有缓存且缓存范围是什么:
包装类 | 包装类 | 基本数据类型 | 包装类 |
---|---|---|---|
Byte | 全部缓存 | Character | c<=127 |
Short | [-128,127] | Float | 没有缓存 |
Integer | [-128,127] | Double | 没有缓存 |
Long | [-128,127] | Boolean | 全部缓存 |
2.5 什么时候触发自动装箱和拆箱
平常我们使用时,这样就会触发自动装箱。
Integer a = 5;
但是使用new Integer的方式则不会触发自动装箱
Integer a = new Integer(5);
再来看一个例子:
Integer a = 1;
int b = 2;
System.out.println(a == b);
// output
true
为什么会是true呢?应该不是同一个对象才对。因为在使用==进行比较时,触发了自动拆箱,此时==比较的是数值(因为都是基本类型)。只有当==两边都是包装类时,比较的才是对象的引用是否一致。
例子无穷多:
Integer a = 1;
Integer b = 2;
Integer c = 3;
Long d = 3L;
System.out.println(c.equals(a+b));
System.out.println(d.equals(a+b));
// output
true
false
a、b、c分别进行拆箱,a+b再进行+运算,再用equals进行比较。为什么会时两个不同的结果,来看下equals源码
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
可以看到,equals先判断类型是否为Integer,再判断值是否相等
最后一个例子:
Integer a = 1;
Integer b = 2;
Long c = 3L;
System.out.println(c == (a+b));
// output
true
a、b、c分别进行拆箱,a和b再进行+运算
Integer a = 1;
Integer b = 1;
System.out.println(a == b);
// output
true
并不会触发自动拆箱!
以上可以总结为:
- 当遇到表达式,且表达式包含运算符时,会进行自动拆箱
- 当==两边都是包装类类型时,比较是否指向同一个对象;当==一边包含基本类型时,就会触发自动拆箱
3 总结
通过上述大概了解了java的如何把基本类型转换为包装类,如果自动进行装箱和拆箱以及装箱过程中需要注意的事项。
参考资料:
- 疯狂java讲义第二版–包装类型篇
- 深入剖析Java中的装箱和拆箱
- jdk1.8源码