天天看点

底层原理_自动装箱与拆箱底层原理

1.自动装箱与拆箱

    Java中的数据类型分为两大类,基本数据类型与引用数据类型。Java中共提供了八种基本数据类型,同时提供了这八种基本数据类型对应的引用数据类型。

    自动装箱:基本数据类型的数据自动转化为对应的引用数据类型的数据

Integer integerVal = 0;
           

    自动拆箱:引用数据类型的数据自动转化为对应的基本数据类型的数据

int intVal = new Integer(0);
           

    基本数据类型变量中存储的就是数据值,但是引用数据类型的变量实际上指向的是此引用对象的地址值,那么按照这种思想,赋值的操作应该是将对应的数据值作为引用地址或者是引用类型对象的地址值赋值给基本数据类型才对,为什么自动装箱与拆箱的操作就能将其转化成对应的数据呢?

    下面就来说一下这种装箱与拆箱的机制到底底层是如何实现的,如何能实现基本数据类型与对应引用数据类型间的自动转化。

2.底层原理

    以下剖析原理以int与Integer类型为例

    自动装箱的底层原理:自动装箱实际上调用的是Integer中的静态方法valueOf(),将基本数据类型的int数值包装成了一个Integer对象

Integer integerVal = 0;//等效于Integer integerVal = Integer.valueOf(0);
           

    自动拆箱的底层原理:自动拆箱的底层实际上调用的是Integer对象的intValue(),得到对象内的int变量的数值,然后给赋值给变量

int intVal = new Integer(0);//等效于int intVal = new Integer(0).intValue();
           

    说到这里,可能很多人知道自动装箱与自动拆箱是与这两个方法有关,但是空说无凭,下面从字节码层面来看一下,为什么大家都说自动装箱与自动拆箱与这两个方法有关,在哪里能证实这两个方法就是自动装箱与拆箱的本质。

3.字节码原理探究

(1)字节码文件

    刚开始学Java的时候,大家应该都做过一件事件,那就是编写.java源文件,然后使用JDK中提供的javac.exe编译器将.java源文件编译成.class文件,class后缀的文件也就是Java中的字节码文件,然后使用java.exe工具来执行字节码文件。

    也就是说JVM中执行的实际上是.class后缀的字节码文件,以下就来解析字节码内容,查看装箱与拆箱的本质(不懂JVM的朋友也没有关系,可以一起看一下从字节码层面来看,自动装箱与拆箱到底字节码中是如何表述的)

(2)自动装箱

    由于字节码文件是以二进制来存储信息的,所以直接打开是看不到内容的,JDK中提供的javap.exe工具来将字节码文件解析成文本

//源码public class MainClass {    public static void main(String[] args) {        Integer integerVal = 0;    }}
           
//javac编译得到字节码javac MainClass.java//javap解析字节码内容(-v -p参数有兴趣的可以自行研究一下)javap -v -p MainClass.clas
           

    使用javap工具解析后得到解析的内容:

底层原理_自动装箱与拆箱底层原理

    这里只关注public static void main()方法内的内容,Code中内部的一行行代码就称之为字节码指令

    可以看到在字节码层面调用了Integer.valueOf()方法。

    看到这里就会发现,其实编译过后,从字节码层面所看的行为与源码的层面还是存在着一定的差异的,这也就是Java为开发人员屏蔽了底层细节,让开发人员更加关注上层的语法,提高开发效率

    从这里也就可以看出,自动装箱的机制确实是调用了Integer.valueOf()方法,将基本数据类型转化为对应的引用数据类型

(3)自动拆箱

    自动拆箱的验证过程与自动装箱的过程一样,需要使用javap.exe工具将字节码内容解析出来

//源码public class MainClass {    public static void main(String[] args) {        Integer integer = new Integer(0);        int intVal = integer;    }}
           
//javac编译得到字节码javac MainClass.java//javap解析字节码内容(-v -p参数有兴趣的可以自行研究一下)javap -v -p MainClass.clas
           

    使用javap工具解析后得到解析的内容:

底层原理_自动装箱与拆箱底层原理

    前面的字节码可以不用看,是创建Integer部分,在标记10的位置,调用了Integer对象的intValue()方法,这也就是自动拆箱的原理,从基本数据类型自动转换到对应的引用数据类型

    以上所说明的是int与Integer类型的自动装箱与拆箱原理,其他的几种基本数据类型也是一样的,装箱与拆箱分别调用的都是XXX.valueOf()与xxxValue()方法,这里就不一一列举的,具体的查看细节同上

    知道了自动装箱与拆箱的原理,大家应该也就能知道下面列举的代码的执行结果了:

Integer integerVal = null;int val = integerVal;
           

4.数据缓存池

(1)面试题

    既然聊了基本数据类型的自动装箱与拆箱这一块,就顺带来聊一聊什么是数据缓存池

    以下是以前看到过的一则面试题:

Integer val1 = 127;Integer val2 = 127;Integer val3 = 128;Integer val4 = 128;System.out.println(val1 == val2);System.out.println(val3 == val4);
           

    刚刚已经聊过了自动装箱问题,也就知道这里实际上是发生了四次装箱,得到了四个Integer对象。

    既然Integer是引用数据类型,那么==号比较的就是这两个引用对象的地址是否相等,但是这里并没有任何的引用之间相互赋值,按理说应该是四个不相同的对象,但是结果却出人意料

底层原理_自动装箱与拆箱底层原理

    根据结果可以推断出val1与val2实际上引用地址是一样的,但是val3与val4又是不一样的对象

    这里的Integer对象都是由Integer的静态方法valueOf()来创建的,所以就需要看一看valueOf()方法里面到底做了什么,是不是单纯地创建对象那么简单。

(2)数据缓存池   

//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);}
           

    可以看到,在valueOf()方法内并不是单纯地创建对象那么简单,而是先判断需要转换的数据是否在low~high范围内,如果在范围内,直接从一个cache数组中取出对应槽位中的Integer对象;如果在此范围内的话,就直接new一个Integer对象返回。

    下面再来看一下IntegerCache的cache数组到底装的是什么:

private static class IntegerCache {    static final int low = -128;    static final int high;    static final Integer cache[];    static {        int h = 127;        //......        high = h;        cache = new Integer[(high - low) + 1];        int j = low;        for(int k = 0; k < cache.length; k++)            cache[k] = new Integer(j++);        //......    }    //......}
           

    IntegerCache是Integer中的一个静态内部类,上述省略了部分代码,只留下了关键性的代码。

    可以看到在此类的静态代码块中会创建cache数组,长度也就是127-(-128)=255,后续接着就是一个循环,也就是初始化输出,数据为-128~127的Integer对象。

    由于static代码块是在程序启动的时候就会运行的,所以也就是说,在程序启动的时候,Integer中的IntegerCache中的cache数组就被初始化为了-128~127之间,一共255个Integer对象。

    再回到valueOf()方法中来看,判断数值是否为-128~127之间,如果是的话,就直接返回cache数组中对应的Integer对象,如果在范围内,才是new一个Integer对象并返回。

    读到这里,也就能说明为什么val1与val2是同一个对象,但是val3与val4并不是同一个对象了:因为val1与val2都是调用valueOf()方法,参数都是127,127属于-128~127之间,所以是直接从IntegerCache.cache数组中取出创建好的Integer对象,并且都是从同一个槽位中取的,所以val1与val2是同一个对象;但是128超出了-128~127的范围,所以不在范围内而直接new了Integer对象,所以val3与val4都是new的不同的Integer对象,故不是同一个对象

    这里的IntegerCache也就是所谓的数据缓存池,起到一定范围内缓存对象的目的,避免项目中重复创建相同的Integer对象,浪费空间又损耗性能

其他数据数据类型的缓存池:

    除了Integer中存在数据缓存池外,其他的还有Byte、Character、Short、Long

    此外Boolean中的数据缓存池比较特殊,由于boolean只有true与false,所以直接定义了两个常量TRUE与FALSE,也就是说,所有使用到的Boolean对象都是TRUE与FALSE两个对象。

5.总结

  • 自动装箱指的是基本数据类型自动转化为对应的引用数据类型
  • 自动拆箱指的是引用数据类型自动转化为对应的基本数据类型
  • 从字节码层面发现,自动装箱实际调用的是XXX.valueOf()方法将对应的数据包装成对应的对象;自动拆箱是实际调用的是对象的xxxValue()方法返回对象内的基本数据类型数据
  • 自动装箱的时候,存在一个数据缓存池问题,数据在一定范围内使用的是缓存池中在程序启动的时候创建好的对象,超出范围才是直接new的对象
  • 此外,除了基本数据类型使用“==”号判断相等外,引用数据类型一定要使用equals()方法来判断相等(除了特殊情况下需要比对是否为同一个对象的情况下)

    最后,极力推荐有时间的朋友可以去学一下JVM(推荐尚硅谷宋红康老师讲的JVM视频),可能学习了JVM你看不到太大实质性地改变,但是对更多的底层原理有了一定的理解,碰到问题的时候也就不需要死记硬背了,从原理出发更好地解决问题

继续阅读