自动装箱和拆箱的原理
我们在面试中经常会被问到什么是自动装箱和拆箱,今天,我就通过代码来讲解一下自动装箱背后的原理
学过Java的都知道,自动装箱呢,就是将基本数据类型自动转换成Integer、Character、Long、Double、Boolean这些包装器类型,那么它们背后的原理是什么呢?我们用下面这些代码示例来做讲解:
首先来看这个例子:
public class Main {
public static void main(String[] args) throws IOException {
Integer a = 10;
Integer b = 10;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);
}
}
我们可以看到输出的结果:
一个是true,一个是false,是不是很奇怪呢?明明两个对象指代的值是相同的,为什么最后的结果不同呢?
这里我们就可以引出来自动装箱的原理了~
如果我们对这些代码进行反编译的话,我们就会发现,编译器在底层自动调用了Integer类的valueOf()方法,那么什么是valueOf()方法呢?我们来点进去看看源码:
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
return i >= -128 && i <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[i + 128] : new Integer(i);
}
这段代码是什么意思呢?
简单来说:就是,在Integer类中本身其实存在一个cache机制,它本身是一个常量,存在于Java的方法区中,当我们给定的整数在[-128,127]之间的话,那么会直接从cache中返回对应的Integer类的引用(或者说地址),如果整数的大小不在这个范围内的话,那么valueof()方法会新new一个Integer类型的对象,并返回。
因此,200大于127,所以每一次返回的包装类型的地址都是不一样的,我们分析出结果原因了~
同样地,我们来看看Double和Boolean类型的自动装箱的背后原理
我们用类似的例子:
public class Main {
public static void main(String[] args) throws IOException {
Double a = 1.0;
Double b = 1.0;
Double c = 2.0;
Double d = 2.0;
System.out.println(a == b);
System.out.println(c == d);
}
}
运行之后,我们可以看到:
两个返回的都是false,我们同样去查看Double的源码:
@HotSpotIntrinsicCandidate
public static Double valueOf(double d) {
return new Double(d);
}
我们可以看到,每一次valueof都会new一个包装类对象,因此每一次对象的引用地址都是新的,所以两个进行比较的话,是不同的
接下来就是Boolean类型:
public class Main {
public static void main(String[] args) throws IOException {
Boolean a = true;
Boolean b = true;
Boolean c = false;
Boolean d = false;
System.out.println(a == b);
System.out.println(c == d);
}
}
话不多说:上结果
可以看到,用Boolean包装类型,返回的结果居然都是true,惯例,去看valueof方法
@HotSpotIntrinsicCandidate
public static Boolean valueOf(boolean b) {
return b ? TRUE : FALSE;
}
我们在源码中发现了两个变量TRUE和FALSE,这两个是个什么东西呢?我们往上面找
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
原来是两个final关键字修饰的静态变量,这样说的话,那么这两个变量的地址都是不变的,也就是说,我们valueof里面return的对象的引用都是相同的~
剩下的Character、Short、Long、Byte基本上和Integer的valueof是类似的,Double、Float的是类似的,Boolean的valueof是单独一组的
以上就是自动装箱的原理了~
接下来我们就讲讲什么是自动拆箱,同样用代码来做讲解,看以下的示例:
public class Main {
public static void main(String[] args) throws IOException {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Long g = 3L;
int int1 = 12;
int int2 = 12;
Integer integer1 = new Integer(12);
Integer integer2 = new Integer(12);
Integer integer3 = new Integer(1);
System.out.println("c==(a+b)"+(c == (a+b)));
System.out.println("g==(a+b)"+(g == (a+b)));
System.out.println("c.equals(a+b)"+c.equals(a+b));
System.out.println("g.equals(a+b)"+g.equals(a+b));
System.out.println("int1 == int2"+(int1 == int2));
System.out.println("int1 == integer1"+(int1 == integer1));
System.out.println("integer1 == integer2"+(integer1 == integer2));
System.out.println("integer3 == a"+(integer3 == a));
}
}
在这里我们先铺垫一下==和equals的区别
对于==来说,又分为了基本数据类型和引用数据类型(包括包装类)还有拆箱(这个等会说)
-
基本数据类型:比较的是值是否形同
-
引用数据类型:比较的是引用是否相同
对于equals来说,也有两种情况,一种是默认情况,还有一种是重写的情况
-
默认情况:比较的是引用是否相同
-
重写情况:有些类在内部对Object类的equals方法进行了重写,比如String类和Integer类,都对该方法进行了重写,因此这种情况下比较的就是值是否相同
我们进到Integer的equals看一看:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return this.value == (Integer)obj;
} else {
return false;
}
}
会发现内部使用的是用==进行比较,这里==号两边一个是基本数据类型,一个是Integer包装类型,因此会进行一个自动拆箱的过程,编译器会调用Integer的intValue()方法,然后比较两个的值,所以这里的equals比较的就是值
其余和Integer同类型的包装类内部有各自对equals方法的重写方式,例如Long:
public boolean equals(Object obj) {
if (obj instanceof Long) {
return this.value == (Long)obj;
} else {
return false;
}
}
铺垫完了==和equals的不同,我们就可以继续来分析自动拆箱了~
-
c == (a+b):这里,== 左边是一个包装类型,右边是两个包装类型之和,我们之前说==如果左右两边是包装类型,那么比较的是引用,但是这里很特殊啊,这里右边是一个操作数表达式,因此这里比较的就是值了
-
g == (a+b):同样的,这里==右边的是一个操作数表达式:因此呢,比较的是值,所以返回true
-
c.equals(a+b):这里用到了equals,equals又比较特殊了,我们说过,Integer和String是重写了equals的,内部是使用了intValue转换成int基本数据类型的,因此比较的是值,而这里由于存在操作数表达式,因此会触发一个自动拆箱和装箱的过程,a和b各自先拆成int类型进行求和,然后编译器会再调用valueof,对求和后的结果进行装箱,然后再回到equals内部比较,由于我们的c是一个Integer类型,我们根据上面对Integer源码的分析可以看到,它内部的equals会判断这个传入进来的类型是否属于Integer,如果也是Integer类型的话,那么就比较值了,所以最后的结果就是true
-
g.equals(a+b):这个和上面差不多,也会触发自动拆箱和装箱的过程,但是呢,这里的g是一个Long包装类型,它的内部也重写了equals,不过它判断的是,传入进来的参数是否是Long类型,如果不是Long包装类型,那么直接就返回false了
-
int1 == int2:这两个都是基本数据类型,那么比较的就是值了,没什么好说的
-
int1 == integer1:这里==左边的是基本数据类型,右边的是包装类型,这个时候,会触发编译器的自动拆箱,调用Integer的intValue方法,因此比较的是值,返回的是true
-
integer1 == integer2:我们在上面分析==和equals的区别的时候讲过,当==左右两边都是包装类型的时候,比较的就是引用,由于这里的integer1和integer2都是new出来的对象,两个的地址肯定是不同的,所以返回的就是false
-
integer3 == a:这里integer3是一个new出来的对象,而a是一个存在于Integer包装类型cache中的常量,他们两个在内存中的位置是不一样的,因此返回false
以上就是关于自动装箱和自动拆箱的原理的解析了~
下一节,我们就来讲讲什么是Java的方法区以及Java的内存模型
转载自:https://juejin.cn/post/7079763194532757518