转载

你真的了解装箱和拆箱吗?

在一名 C++ 程序员看来,Java 中很多名词听着高大上,实则非常简单,装箱和拆箱尤甚。来看下装箱和拆箱的定义:装箱是指将基本类型转化成包装器类型,拆箱是指将包装器类型转化成基本数据类型。

Java 有八种基本数据类型 byte/char/boolean/short/int/long/float/double 。在一切皆对象的 Java 世界里,当然也少不了它们的对象包装类型 Byte/Char/Boolean/Short/Integer/Long/Float/Double。用一张图表达它们的关系最简单不过:

你真的了解装箱和拆箱吗?

从代码层面看,装箱与拆箱演示示例代码 1 如下:

public class BoxTest {
    public static void main(String[] args) {
        Long objectLong100 = 100L;
        long baseLong100 = objectLong100;
    }
}
复制代码

其中 Long objectLong100 = 100L; 对 100L 进行装箱成 Long 类型并赋给 objectLong100 对象类型变量,同理 long baseLong100 = objectLong100; 对objectLong100 进行了拆箱并赋值给基本类型变量 baseLong100。

自动装箱和拆箱

什么是自动装箱和拆箱

从示例代码 1 可以看到基本类型和包装类型之间的转换可自动进行。用 javap 反编译上述代码可看到如下字节码:

你真的了解装箱和拆箱吗?

从字节码可以看出,示例代码 1 被编译器改写成如下形式:

public class BoxTest {
    public static void main(String[] args) {
        Long objectLong100 = Long.valueOf(100L);
        long baseLong100 = objectLong100.longValue();
    }
}
复制代码

这就是 Java 语法糖之一的(Java 语法糖还有很多,可参考《 Hollis原创|不了解这12个语法糖,别说你会Java 》---自动装箱和拆箱,它省去了程序员显示的用代码在基本类型和包装器类型之间进行转化。

何时进行自动装箱和拆箱

自动装箱的情景比较单一: 将基本类型赋值给包装器类型的时候,这里的赋值不一定是 =/-=/+= 符号,也可以是方法调用 会触发自动装箱。如下:

public class BoxTest {

    public static void main(String[] args) {
        //= 赋值自动装箱
        Long objectLong = 100L;

        //equals 调用自动装箱
        if (objectLong.equals(100L)) {
            System.out.println("objectLong.equals(100L)");
        }
        
        //add 方法调用自动装箱
        List<Long> objectLongList = new ArrayList<>();
        objectLongList.add(objectLong);
    }
}
复制代码

同理,将包装器类型赋值给基本类型的时候会发生自动拆箱:

public class BoxTest {

    public static void unpacking(long var) {
        System.out.println("var = " + var);
    }
    
    public static void main(String[] args) {
        Long objectLong = 100L;

        //= 赋值自动拆箱
        long basicLong = objectLong;

        //方法调用自动拆箱
        unpacking(objectLong);
    }
}
复制代码

但与自动装箱不同,自动拆箱还会在如下情况发生:

1、当包装器类型之间或者包装器类型与基本类型之间进行数学运算(+、-、*、/、++、--等)的时候。

2、当包装器类型与基本类型之间进行 ==、!= 比较操作时候。

其中 1 很好理解,毕竟数学运算(想想 CPU 操作)只能在基本类型之间进行而不能在对象之间进行。2 举个例子来简单说明:

public class BoxTest {

    public static void main(String[] args) {

        Long objectLong = 500L;
        long basicLong = 500L;

        if (objectLong == basicLong) {
            System.out.println("objectLong == basicLong");
        } else {
            System.out.println("objectLong != basicLong");
        }
    }
}
复制代码

执行输出 objectLong == basicLong ,这是在基本类型与包装器类型之间进行比较,发生了拆箱操作,所以输出相等。 如果还是以为 basicLong 会被装箱成包装器类型再与 objectLong 比较,那就大错特错了

自动装箱和拆箱中的"陷阱"

自动装箱和拆箱大法好,但是其中也存在很多"陷阱",且听我抽丝剥茧,一一道来。来看一段代码:

public class BoxTest {
    public static void main(String[] args) {

        Long objectLong1 = new Long(10);
        Long objectLong2 = new Long(10);

        Long objectLong11 = 10L;
        Long objectLong22 = 10L;

        Long objectLong5 = 1000L;
        Long objectLong6 = 1000L;

        Long objectLong3 = new Long(10);
        long basicLong = 10L;
        
        if(objectLong1 == objectLong2) {
            System.out.println("objectLong1 == objectLong2");
        } else {
            System.out.println("objectLong1 != objectLong2");
        }

        if(objectLong11 == objectLong22) {
            System.out.println("objectLong11 == objectLong22");
        } else {
            System.out.println("objectLong11 != objectLong22");
        }

        if(objectLong5 == objectLong6) {
            System.out.println("objectLong1 == objectLong2");
        } else {
            System.out.println("objectLong1 != objectLong2");
        }

        if (objectLong3 == basicLong) {
            System.out.println("objectLong3 == basicLong");
        } else {
            System.out.println("objectLong3 != basicLong");
        }
    }
}
复制代码

先用笔写下你认为的输出的结果,再看我下面截图中的输出,看你"执行"的对不对。运行结果如下:

你真的了解装箱和拆箱吗?

1、objectLong1 与 objectLong2 的比较结果不出意外,因为他们是两个包装器类型变量。由于它们并不是同一个对象,所以并不相等。

2、objectLong3 与 basicLong 的比较根据 何时进行自动装箱和拆箱 讲到的拆箱的场景也可以快速理解。

3、objectLong5 与 objectLong6 比较不相等大部分人还能推理的到:objectLong5 与 objectLong6 都发生自动装箱成包装器对象,两个对象之间的 == 比较并不相等。但 objectLong11 与 objectLong22 的比较输出结果恐怕要让很多人大跌眼镜。 因此我们来看下 Long 自动装箱时调用的方法 Long.valueOf(long value) 的源代码:

public final class Long extends Number implements Comparable<Long> {
    //......此处省略一堆
    private static class LongCache {
        private LongCache(){}

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

        static {
            for(int I = 0; I < cache.length; I++)
                cache[I] = new Long(I - 128);
        }
    }
    //......此处省略一堆
    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
    //......此处省略一堆
}
复制代码

可以看出当要进行自动装箱的基本类型值在 [-128~127] 之间的时候,直接将 LongCache 中缓存的对象返回,而 LongCache.cache 是一个数组,里面保存了值为 -128~127 的包装器对象,在 Long.class 第一次加载的时候分配。 这就解释了为什么 10L 自动装箱成两个对象,他们之间是相等的。而 1000L 自动装箱成两个对象却不相等。前者用的是 LongCache.cache 中缓存中的同一个对象,后者每次自动装箱都 new 一个新对象。

其他包装器类型在装箱时都采用了类似的缓存方法,防止生成太多对象,但 Float 和 Double 例外。自动装箱方法大体上分为以下三种:

  • 可穷举的,如 Byte/Character/Boolean,缓存了所有可能值的对象。所以他们所有相同值的包装器对象之间的比较都是相等的,而大部分比较是不相等的。具有不确定性。
  • 不可穷举不连续的,如 Short/Integer/Long,只缓存了范围内的常用值对象,所有他们部分相同值的包装器对象之间的比较是相等的。
  • 不可枚举且连续的,如 Float/Double,每次装箱都是 new 一个新对象。

感兴趣的可以点进包装类源码中进行查看,比较简单。

总结和建议

1、包装器类型本质上时一个对象,自动装箱时先在堆中 new 一个对象,再将基本类型的值赋给该对象中的 value。

2、一定不要使用 ==/!= 在包装器类型之间、包装器与基本类型之间进行比较。请使用 equals 方法,它并不能保证始终正确。

3、当进行数学运算时,遇到包装器类型会进行自动拆箱,再对基本类型进行运算。

原文  https://juejin.im/post/5d1610ae6fb9a07eba2c511d
正文到此结束
Loading...