记录生活中的点点滴滴

0%

Java的自动装箱与自动拆箱

这一篇文章来谈谈Java的自动装箱与自动拆箱。

我们先来看看什么是自动装箱?什么是自动拆箱?我直接上代码了:

1
2
3
4
5
6
7
public void Demo1(){
//自动装箱
Integer num = 66;

//自动拆箱
int my_num = num;
}

上面两行代码就是自动装箱与自动拆箱。

装箱过程中,系统为我们执行了: Integer num = Integer.valueOf(66);

拆箱过程中,系统为我们执行了: int my_num = num.intValue();

简单一点 说,自动装箱就是自动将基本数据类型转换成包装器类型;自动拆箱就是自动将包装器类型转换成基本数据类型。

需要装箱拆箱的类型如下图:

我们以 Integer 为例,分析一下它的源码:

先看看 Integer.valueOf() 函数:

1
2
3
4
5
6
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

如果 i 在 lowhigh 之间,在返回缓存中的一个值;否则返回新的构造对象。

还有拆箱过程的 intValue() 函数:

1
2
3
4
@HotSpotIntrinsicCandidate
public int intValue() {
return value;
}

我们自己再看看 Integer 的构造函数,和 cache 函数,我们可以得出以下结论:

1、i >= -128 && i <= 127 =====> IntegerCache.cache[i + (-IntegerCache.low)];

2、i > 127 || i < -128 =====> new Integer(i);

cache里的值应该是本来就创建好了,也就是说在 i > 127 || i < -128是会创建不同的对象,在 i >= -128 && i <= 127 会根据i的值返回已经创建好的指定的对象。

再来举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void Demo2(){
Integer n1 = 100;
Integer n2 = 100;
Integer n3 = 200;
Integer n4 = 200;

//n1 n2在缓存中取,它们的地址相同
System.out.println(n1 == n2); //true
//n3 n4是新构造的Integer对象,它们地址肯定不同呀
System.out.println(n3 == n4); //false
}

n1 n2 n3 n4都会进行自动装箱,但是前两个是在缓存中取,地址相同;后两个是新构造的对象,地址不同。

看完了 Integer,再来看看 Double

1
2
3
4
5
6
7
8
9
10
@Test
public void Demo3(){
Double n1 = 100.0;
Double n2 = 100.0;
Double n3 = 200.0;
Double n4 = 200.0;

System.out.println(n1 == n2); //false
System.out.println(n3 == n4); //false
}

为什么 Double 全都是 false 呢?让我们看看它的 valueOf() 函数源码:

1
2
3
4
@HotSpotIntrinsicCandidate
public static Double valueOf(double d) {
return new Double(d);
}
1
2
3
public static Double valueOf(String s) throws NumberFormatException {
return new Double(parseDouble(s));
}

这个很好理解,因为对于Integer,在 [-128,127]之间只有固定的256个值,所以为了避免多次创建对象,我们事先就创建好一个大小为256的Integer数组cache缓存,所以如果值在这个范围内,就可以直接返回我们事先创建好的对象就可以了。

但是对于Double类型来说,我们就不能这样做,因为它在这个范围内个数是无限的。

总结一句就是:在某个范围内的整型数值的个数是有限的,而浮点数却不是。

所以我们看源码就能看出,Double的构造方法很直接,直接返回新的对象,所以每次创建的对象都不一样。

下面我们进行一个归类:

Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。

Double派别:Double、Float的valueOf方法的实现是类似的。每次都返回不同的对象。

还有一个 Boolean 类型,我们直接通过代码来说明:

1
2
3
4
5
6
7
8
9
10
@Test
public void Demo4(){
Boolean b1 = false;
Boolean b2 = false;
Boolean b3 = true;
Boolean b4 = true;

System.out.println(b1 == b2); //true
System.out.println(b3 == b4); //true
}

这个很容易说明,Boolean就两种类型,源码肯定先把这两种类型都已经缓存ok了,所以都为true。

下面我们再谈谈它们的 ==equals() 方法:

我先说结论,再上代码:

  • 当一个基本数据类型与包装类进行 == + - * / 运算时,会将包装类拆箱,在进行值的比较或运算。

  • 进行 equals() 比较时,必须两个条件满足才为 true:类型相同、值相等。如果基本类型与包装类比较,则类型相同的意思是基本类型进行自动装箱后与包装类是否类型相同。这些包装类的equals源码基本都是如下所示:

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

然后我们上代码来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void Demo5(){
Integer n1 = 200;
int n2 = 200;
System.out.println(n1 ==n2); //true
System.out.println(n1.equals(n2)); //true

Double n3 = 200.0;
double n4 = 200.0;
System.out.println(n3 == n4); //true
System.out.println(n3.equals(n4)); //true

Integer n5 = 100;
int n6 = 200;
Long n7 = 300L;
System.out.println(n7 == (n5+n6)); //true
System.out.println(n7.equals(n5+n6)); //false
}

这三个在进行 == 比较的时候,因为有基本数据类型的存在,包装类会拆箱转换成基本数据类型,然后再进行数值的比较,所以都为 true。

前两个在进行 equals() 比较的时候,由于基本数据类型进行装箱后,它们类型相同,数值相等,所以都返回 true,但是第三个基本数据类型装箱后为 Integer 类型,与 Long 类型不符,尽管它们数值相等,但是因为类型不同,所以返回 false。

通过上面的分析我们需要知道两点:

1、什么时候会引发装箱和拆箱

2、装箱操作会创建对象,频繁的装箱操作会消耗许多内存,影响性能,所以可以避免装箱的时候应该尽量避免。