java拆箱与装箱机制

Java有8种基本类型,每种基本类型又有对应的包装类型。在Java中,一切都以对象作为基础,但是基本类型并不是对象,如果想以对象的方式使用这8中基本类型,可以将它们转换为对应的包装类型。基本类型和包装类型的对应:

int(4字节) Integer
byte(1字节) Byte
short(2字节) Short
long(8字节) Long
float(4字节) Float
double(8字节) Double
char(2字节) Character
boolean(未定) Boolean

Java 5增加了自动装箱与自动拆箱机制,方便基本类型与包装类型的相互转换操作。在Java 5之前,如果要将一个int型的值转换成对应的包装器类型Integer,必须显式的使用new创建一个新的Integer对象,或者调用静态方法Integer.valueOf()。
//在Java 5之前,只能这样做
Integer value = new Integer(10);
//或者这样做
Integer value = Integer.valueOf(10);
//直接赋值是错误的
//Integer value = 10;
在Java 5中,可以直接将整型赋给Integer对象,由编译器来完成从int型到Integer类型的转换,这就叫自动装箱。

//在Java 5中,直接赋值是合法的,由编译器来完成转换
Integer value = 10;
与此对应的,自动拆箱就是可以将包装类型转换为基本类型,具体的转换工作由编译器来完成。
//在Java 5 中可以直接这么做
Integer value = new Integer(10);
int i = value;
自动装箱与自动拆箱为程序员提供了很大的方便,而在实际的应用中,自动装箱与拆箱也是使用最广泛的特性之一。自动装箱和自动拆箱其实是Java编译器提供的一颗语法糖(语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通过可提高开发效率,增加代码可读性,增加代码的安全性)。
1 实现

在八种包装类型中,每一种包装类型都提供了两个方法:

静态方法valueOf(基本类型):将给定的基本类型转换成对应的包装类型;

实例方法xxxValue():将具体的包装类型对象转换成基本类型;
下面我们以int和Integer为例,说明Java中自动装箱与自动拆箱的实现机制。看如下代码:

class Auto //code1
{
public static void main(String[] args)
{
//自动装箱
Integer inte = 10;
//自动拆箱
int i = inte;

//再double和Double来验证一下
Double doub = 12.40;
double d = doub;

}
}
上面的代码先将int型转为Integer对象,再讲Integer对象转换为int型,毫无疑问,这是可以正确运行的。可是,这种转换是怎么进行的呢?使用反编译工具,将生成的Class文件在反编译为Java文件,让我们看看发生了什么:
class Auto//code2
{
public static void main(String[] paramArrayOfString)
{
Integer localInteger = Integer.valueOf(10);

int i = localInteger.intValue();


Double localDouble = Double.valueOf(12.4D);
double d = localDouble.doubleValue();
}
}
我们可以看到经过javac编译之后,code1的代码被转换成了code2,实际运行时,虚拟机运行的就是code2的代码。也就是说,虚拟机根本不知道有自动拆箱和自动装箱这回事;在将Java源文件编译为class文件的过程中,javac编译器在自动装箱的时候,调用了Integer.valueOf()方法,在自动拆箱时,又调用了intValue()方法。我们可以看到,double和Double也是如此。
实现总结:其实自动装箱和自动封箱是编译器为我们提供的一颗语法糖。在自动装箱时,编译器调用包装类型的valueOf()方法;在自动拆箱时,编译器调用了相应的xxxValue()方法。

2 自动装箱与拆箱中的“坑”

在使用自动装箱与自动拆箱时,要注意一些陷阱,为了避免这些陷阱,我们有必要去看一下各种包装类型的源码。

Integer源码

public final class Integer extends Number implements Comparable<Integer> {
private final int value;

/*Integer的构造方法,接受一个整型参数,Integer对象表示的int值,保存在value中*/
public Integer(int value) {
this.value = value;
}

/*equals()方法判断的是:所代表的int型的值是否相等*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

/*返回这个Integer对象代表的int值,也就是保存在value中的值*/
public int intValue() {
return value;
}

/**
* 首先会判断i是否在[IntegerCache.low,Integer.high]之间
* 如果是,直接返回Integer.cache中相应的元素
* 否则,调用构造方法,创建一个新的Integer对象
*/
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

/**
* 静态内部类,缓存了从[low,high]对应的Integer对象
* low -128这个值不会被改变
* high 默认是127,可以改变,最大不超过:Integer.MAX_VALUE - (-low) -1
* cache 保存从[low,high]对象的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) {
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);
}
high = h;

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

private IntegerCache() {}
}
}
以上是Oracle(Sun)公司JDK 1.7中Integer源码的一部分,通过分析上面的代码,得到:
1)Integer有一个实例域value,它保存了这个Integer所代表的int型的值,且它是final的,也就是说这个Integer对象一经构造完成,它所代表的值就不能再被改变。
2)Integer重写了equals()方法,它通过比较两个Integer对象的value,来判断是否相等。
3)重点是静态内部类IntegerCache,通过类名就可以发现:它是用来缓存数据的。它有一个数组,里面保存的是连续的Integer对象。
   (a) low:代表缓存数据中最小的值,固定是-128。
   (b) high:代表缓存数据中最大的值,它可以被该改变,默认是127。high最小是127,最大是Integer.MAX_VALUE-(-low)-1,如果high超过了这个值,那么cache[ ]的长度就超过Integer.MAX_VALUE了,也就溢出了。
   (c) cache[]:里面保存着从[low,high]所对应的Integer对象,长度是high-low+1(因为有元素0,所以要加1)。
4)调用valueOf(int i)方法时,首先判断i是否在[low,high]之间,如果是,则复用Integer.cache[i-low]。比如,如果Integer.valueOf(3),直接返回Integer.cache[131];如果i不在这个范围,则调用构造方法,构造出一个新的Integer对象。
5)调用intValue(),直接返回value的值。
通过3)和4)可以发现,默认情况下,在使用自动装箱时,VM会复用[-128,127]之间的Integer对象。

Integer a1 = 1;
Integer a2 = 1;
Integer a3 = new Integer(1);
//会打印true,因为a1和a2是同一个对象,都是Integer.cache[129]
System.out.println(a1 == a2);
//false,a3构造了一个新的对象,不同于a1,a2
System.out.println(a1 == a3);
Byte源码

public final class Byte extends Number implements Comparable<Byte> {
//Byte表示的范围是[-128,127]
public static final byte MIN_VALUE = -128;
public static final byte MAX_VALUE = 127;

private final byte value;

public Byte(byte value) {
this.value = value;
}

/**
* 缓存Byte对象
* 将Byte可能的256个对象全部保存到cache[]中
* @author cxy
*
*/
private static class ByteCache {
private ByteCache(){}

static final Byte cache[] = new Byte[-(-128) + 127 + 1];

static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Byte((byte)(i - 128));
}
}

/*直接返回ByteCache.cache[]中相应的对象*/
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}

/*返回此对象的byte值*/
public byte byteValue() {
return value;
}
}
byte的表示范围是[-128,127],在Byte内部同样有一个ByteCache类,它也同样有一个cache[ ],它里面保存了所有可能的256个Byte对象。所以在自动装箱时,所有的Byte对象都是复用ByteCache.cache[ ]中的元素。
同样的Character中的CharacterCache类也有一个cache[ ],缓存了[0,127]中的元素。Short和Integer一样,缓存了[-128,127]之间的数,不同的是,Integer可以修改high的值,ShortCache中则是写死的,不能改变。Long的实现方法和Short一样。

Double和Float

/*Double.valueOf(double d)*/
public static Double valueOf(Double d) {
return new Double(d);
}

/*Float.valueOf(float f)*/
public static Float valueOf(float f) {
return new Float(f);
}
从源码中可以看出,Double和Float都没有缓存了,调用valueOf()方法,直接构造出一个新的对象。Double和Float之所以不用缓存,是因为没有办法缓存,(0,1)这么小的一个区间里面,就有无数个double或float数,根本无从缓存。所以在使用Double和Float自动装箱时,全都是构造新的对象,没有缓存。
Boolean源码

public final class Boolean implements java.io.Serializable,Comparable<Boolean>
{
/*boolean只有两种取值:true,false,所以不需要内部类来缓存了*/
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

private final boolean value;

public Boolean(boolean value) {
this.value = value;
}

public boolean booleanValue() {
return value;
}

/**
* 根据b的值,返回对应的对象
*/
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
查看Boolean的源码,发现Boolean没有无参的valueOf(),我们可以推断Boolean没有自动装箱与封箱,可以通过代码验证一下:
boolean b = true;
Boolean b1 = b;
boolean b2 = b1;
这些代码是无法通过编译的。
Boolean还是用到了缓存,由于boolean只有两种取值,所以没有必要使用内部类或者数组来保存缓存的对象,直接定义两个静态属性即可,也就是Boolean.TRUE和Boolean.FALSE。在调用Boolean.valueOf(boolean b)是,返回的是缓存的TRUE或者FALSE,代码验证:

Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
Boolean b3 = new Boolean(true);
//true,因为返回的都是TRUE对象
System.out.println(b1 == b2);
//false,因为b1是TRUE,b3则是一个新的Boolean对象
System.out.println(b1 == b3);
发生时机

来欣赏一个比较典型的例子:

public class AutoWrapperTrap {
public static void main(String[] args) {
//[-128,127]之间,自动装箱会复用对象
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
//不会复用
Integer e = 321;
Integer f = 321;

int base = 3;

Long g = 3L;

System.out.println(c == base);//true c自动拆箱
System.out.println(c == d);//true
System.out.println(e == f);//false
System.out.println(c == (a + b));//true 遇到算术运算,自动拆箱
System.out.println(c.equals(a + b));//true 需要对象,自动装箱
System.out.println(g == (a + b));//true
System.out.println(g.equals(a + b));//false 只会自动装箱为对应的包装类型
}
}
通过反编译后,得到如下代码:
public class AutoWrapperTrap
{
  public static void main(String[] args)
  {
    Integer a = Integer.valueOf(1);
    Integer b = Integer.valueOf(2);
    Integer c = Integer.valueOf(3);
    Integer d = Integer.valueOf(3);


    Integer e = Integer.valueOf(321);
    Integer f = Integer.valueOf(321);


    int base = 3;


    Long g = Long.valueOf(3L);


    System.out.println(c.intValue() == base);
    System.out.println(c == d);
    System.out.println(e == f);
    System.out.println(c.intValue() == a.intValue() + b.intValue());
    System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue())));
    System.out.println(g.longValue() == a.intValue() + b.intValue());
    System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));
  }
}
通过上面的代码,我们分析一下自动装箱与拆箱发生的时机:
(1)当需要一个对象的时候会自动装箱,比如Integer a = 10;equals(Object o)方法的参数是Object对象,所以需要装箱。

(2)当需要一个基本类型时会自动拆箱,比如int a = new Integer(10);算术运算是在基本类型间进行的,所以当遇到算术运算时会自动拆箱,比如代码中的 c == (a + b);

(3) 包装类型 == 基本类型时,包装类型自动拆箱;

需要注意的是:“==”在没遇到算术运算时,不会自动拆箱;基本类型只会自动装箱为对应的包装类型,代码中最后一条说明的内容。

总结

在JDK 1.5中提供了自动装箱与自动拆箱,这其实是Java 编译器的语法糖,编译器通过调用包装类型的valueOf()方法实现自动装箱,调用xxxValue()方法自动拆箱。自动装箱和拆箱会有一些陷阱,那就是包装类型复用了某些对象。

(1)Integer默认复用了[-128,127]这些对象,其中高位置可以修改;

(2)Byte复用了全部256个对象[-128,127];

(3)Short服用了[-128,127]这些对象;

(4)Long服用了[-128,127];

(5)Character复用了[0,127],Charater不能表示负数;

Double和Float是连续不可数的,所以没法复用对象,也就不存在自动装箱复用陷阱。

Boolean没有自动装箱与拆箱,它也复用了Boolean.TRUE和Boolean.FALSE,通过Boolean.valueOf(boolean b)返回的Blooean对象要么是TRUE,要么是FALSE,这点也要注意。

本文介绍了“真实的”自动装箱与拆箱,为了避免写出错误的代码,又从包装类型的源码入手,指出了各种包装类型在自动装箱和拆箱时存在的陷阱,同时指出了自动装箱与拆箱发生的时机。
————————————————
版权声明:本文为CSDN博主「喻红叶」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yuhongye111/java/article/details/31850779

上一篇:Java 判断字符串是否为数字(浮点类型也包括)


下一篇:Char - JAVA