【从零到Offer】- 拆箱装箱那点小事

259 阅读4分钟

引言

在介绍本期文章内容之前,让我们先来看一小段代码:

     int a = 10;
     Integer b = 10;
     if(b == a){
         System.out.println("相等");
     }

执行结果应该大家是毋庸置疑的,10等于10,自然会输出相等。但是有一个问题,a明明是int类型,而b则是Integer类型。两个明显是不同类型的对象,为什么能够相等呢?这就涉及到我们这次要介绍的内容:拆箱、装箱

简介

首先,简单介绍下装箱、拆箱的含义:

  • 装箱,指基础类型(int、double等)转换成包装类型(Integer、Double等)的过程
  • 拆箱,顾名思义同装箱相反,是从包装类型转换到基础类型的过程。

在JDK1.5以前,还不存在装箱、拆箱的概念。因此,对于基本类型同包装类型的转换,基本只能通过代码手写实现。

     Boolean a = Boolean.valueOf(true);
     Integer b = Integer.valueOf(10);

然而,懒惰是推动人类社会进步的动力。时间一长了,大家就发现:

“明明这两个类型都是表达相似含义的东西,有没有什么办法把这两个的转换隐藏起来呢?这样我就不用写着么多代码了呀”

实现原理

结合上述对于拆箱装箱的介绍,我们不难分析出来,要实现拆箱和装箱,至少需要考虑两点:

  1. 什么时候做拆箱装箱?
  2. 怎么做拆箱装箱?

啥时候拆箱装箱

首先明确一点,装箱和拆箱是指发生在基本类型与包装类型之间的。如果一个类根本都不存在包装类型 or 基本类型,那么肯定是不会发生拆箱装箱的。在Java中,同时存在基本类型和包装类型的有如下几个:

基本类型包装类型
booleanBoolean
byteByte
intInteger
shortShort
charCharacter
longLong
floatFloat
doubleDouble

那么在知道了有哪些类型存在包装类和基本类后。我们还需要知道装箱和拆箱触发的时机。简单来说主要分为以下几个:

1、赋值

2、调用方法

3、数值计算

设值,这个主要就是讲的将基本类型的数值赋值给包装类型,如下代码所示:

     Integer b = 10;

调用方法,主要指的是方法的入参为包装类型,传入的实际上是基本类型的情况,以Integer类型为例子,调用方法产生装箱拆箱的情况如下所示:

     public static void main(String[] args) {
         //产生装箱、拆箱
         transform(1);
     }
 ​
     public static void transform(Integer b){
             System.out.println(b);
     }

最后一种情况,主要指的是基本类型同包装类型做计算的时候会发生拆箱。具体例子如下所示:

     public static void main(String[] args) {
         Integer b = 10;
         int ab = 20;
         int i = b + ab; // b调用拆箱的方法
     }

怎么做拆箱装箱

装箱

在明确了拆箱装箱的时机以后,就是考虑如何实现转化了。实际上,在由编译.java的过程中,编译器会将特定的包装类型调用对应的转化方法,下面以具体的代码为例子:

     Boolean a = true;
     Integer b = 10;

通过将上述代码的.class文件进行反编译(调用代码javap -c xxx.class),可以得到如下的反编译代码:

 public class com.example.demo.DemoApplication {
   public com.example.demo.DemoApplication();
     Code:
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: return
 
   public static void main(java.lang.String[]);
     Code:
        0: iconst_1
        1: invokestatic  #2                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
        4: astore_1
        5: bipush        10
        7: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       10: astore_2
       11: goto          17
       14: astore_1
       15: aload_1
       16: athrow
       17: return
     Exception table:
        from    to  target type
            0    11    14   Class java/lang/Throwable
 
   static {};
     Code:
        0: ldc           #6                  // class com/example/demo/DemoApplication
        2: invokestatic  #7                  // Method org/slf4j/LoggerFactory.getLogger:(Ljava/lang/Class;)Lorg/slf4j/Logger;
        5: putstatic     #8                  // Field LOGGER:Lorg/slf4j/Logger;
        8: return
 }

可以看到,装箱实际上调用的分别是Boolean.valueOfInteger.valueOf的代码。这里我们具体以Integer.valueOf展开来看具体的实现:

     public static Integer valueOf(int i) {
         return new Integer(i);
     }

valueOf最简单的实现就是直接根据传入的int类型生成一个Integer对象。但是仔细想想,这样真的好吗?假设我们有一个计算密集型的系统,每天都会执行许多次的对象创建。由此会带来频繁的GC,对于JVM会产生较大的压力。

这也是为什么IDEA针对一些装箱的操作,会给出相应的warning提示信息,会建议将Integer类型转换成基本类型(即int)。

image-20221207200828021.png

因此,Integer类中自然也对这种情况做了考虑。Integer类在实现的时候,会将常用范围的数据缓存下来(默认的范围是[-128,127]),当传进来的数字处于这个范围的时候,就直接获取缓存好的数据值,减少对象的生成,进而减少GC的次数。实现代码如下所示:

     public static Integer valueOf(int i) {
         if (i >= IntegerCache.low && i <= IntegerCache.high)
             return IntegerCache.cache[i + (-IntegerCache.low)];
         return new Integer(i);
     }

需要注意的是,并非所有包装类都能够实现缓存逻辑。例如Double、Float等浮点数类型,由于小数在一个范围内是无穷多的。因此这些都不会做缓存处理。

拆箱

说完装箱,我们简单聊聊拆箱。拆箱的实现其实也并不复杂,主要就是调用包装对象里的拆箱方法。以Integer类为例子,其主要调用的就是Integer.intValue方法,具体源代码如下所示:

     public int intValue() {
         return value;
     }

总结

本文主要介绍了拆箱、装箱的实现原理及机制。这个知识点本身不是特别复杂的实现内容,算是JAVA编译器优化的一个小小的语法糖。但是了解了其原理和机制,有助于我们在未来遇到问题的时候,更快地排查出问题的原因。


想了解更多【面试内容分享】以及【干货源码介绍】,欢迎关注公众号:【笑傲菌】

qrcode_for_gh_11a93a348da0_344.jpg

参考文献

Java基础之拆箱和装箱

Java中的自动装箱与拆箱