Java 中的拆箱装箱—这一篇全了解

为何需要设计装箱和拆箱

你好,我是必应。我可以帮你解释一下java为什么要设计装箱和拆箱。😊

装箱和拆箱是java为了实现内存分配策略而产生的必要功能。¹

java中有两种类型:基本数据类型(primitive type)和引用类型(reference type)。基本数据类型是直接存储在栈(stack)中的,而引用类型是存储在堆(heap)中的,栈中只存储一个指向堆中对象的地址。¹

为了让基本数据类型能够像引用类型一样被当作对象使用,就需要将它们转换成对应的wrapper类对象,这个过程叫做装箱(boxing)。²⁵

反之,为了让wrapper类对象能够像基本数据类型一样被当作值使用,就需要将它们转换成对应的基本数据类型值,这个过程叫做拆箱(unboxing)。²⁵

java设计装箱和拆箱有以下几个原因:

  • 装箱和拆箱可以让基本数据类型和引用类型之间有一个统一的视角,也就是说任何一个值都可以被当作一个对象处理。²
  • 装箱和拆箱可以简化编程的过程,不需要显式地构造或者转换对象。⁴⁶
  • 装箱和拆箱可以提高代码的可读性和可维护性,不需要关心变量的具体类型。⁴⁶

但是装箱和拆箱也有一些缺点:

  • 装箱和拆箱会增加内存消耗和垃圾回收的开销,因为每次装箱都会创建一个新的对象,并且每次拆箱都会丢弃一个对象。³
  • 装箱和拆箱会降低运行效率,因为每次装箱或者拆箱都需要进行额外的操作。³
  • 装箱和拆箱可能会导致一些意想不到的错误或者异常,比如空指针异常(NullPointerException),数学异常(ArithmeticException),或者比较结果不正确等等。⁵

一. 什么是装箱?什么是拆箱?

Java 为每种基本数据类型都提供了对应的包装器类型,让基本类型具备对象的特征**,** 实现更多的功能**.**。在 Java SE5 之前,如果要生成一个数值为 10 的 Integer 对象,必须这样进行:

Integer i = new Integer(10);

而在从 Java SE5 开始就提供了自动装箱的特性,如果要生成一个数值为 10 的 Integer 对象,只需要这样就可以了:

Integer i = 10

  这个过程中会自动根据数值创建对应的 Integer 对象,这就是装箱。

拆箱跟装箱对应,就是自动将包装器类型转换为基本数据类型:

Integer i = 10;  //装箱
 
int n = i;   //拆箱
 

总结:

  装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型,有了装箱拆箱,java 可以根据上下文,自动进行转换,极大的简化相关编程。自动装箱实际上算是一种语法糖,装箱拆箱发生在代码编译阶段  下表是基本数据类型对应的包装器类型:

int(4 字节)

Integer

byte(1 字节)

Byte

short(2 字节)

Short

long(8 字节)

Long

float(4 字节)

Float

double(8 字节)

Double

char(2 字节)

Character

boolean(未定)

Boolean

二. 装箱和拆箱是如何实现的

以 Interger 类为例,下面看一段代码:

public class Main {
 
    public static void main(String[] args) {
        Integer i = 10;
        int n = i;
    }
}
 

  反编译 class 文件之后得到如下内容:

  从反编译得到的字节码内容可以看出,在装箱的时候自动调用的是 Integer 的 valueOf(int) 方法。而在拆箱的时候自动调用的是 Integer 的 intValue 方法。

  其他的也类似,比如 Double、Character,不相信的朋友可以自己手动尝试一下。

  因此可以用一句话总结装箱和拆箱的实现过程:

  装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。(xxx 代表对应的基本数据类型)。

java8 种基本类型的自动装箱代码实现。如下:

//boolean原生类型自动装箱成Boolean
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
 
 
    //byte原生类型自动装箱成Byte
    public static Byte valueOf(byte b) {
        final int offset = 128;
        return ByteCache.cache[(int)b + offset];
    }
 
 
    //short原生类型自动装箱成Short
    public static Short valueOf(short s) {
        final int offset = 128;
        int sAsInt = s;
        if (sAsInt >= -128 && sAsInt <= 127) { // must cache
            return ShortCache.cache[sAsInt + offset];
        }
        return new Short(s);
    }
 
    //char原生类型自动装箱成Character
    public static Character valueOf(char c) {
        if (c <= 127) { // must cache
            return CharacterCache.cache[(int)c];
        }
        return new Character(c);
    }
 
    //int原生类型自动装箱成Integer
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
 
    //long原生类型自动装箱成Long
    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);
    }
 
    //double原生类型自动装箱成Double
    public static Double valueOf(double d) {
        return new Double(d);
    }
 
 
    //float原生类型自动装箱成Float
    public static Float valueOf(float f) {
        return new Float(f);
    }

通过分析源码发现,只有 double 和 float 的自动装箱代码没有使用缓存,每次都是 new 新的对象,其它的 6 种基本类型都使用了缓存策略。
    使用缓存策略是因为,缓存的这些对象都是经常使用到的(如字符、-128 至 127 之间的数字),防止每次自动装箱都创建一次对象的实例。
    而 double、float 是浮点型的,没有特别的热的(经常使用到的)数据的,缓存效果没有其它几种类型使用效率高。

三. 相关的问题

  1. 下面这段代码的输出结果是什么?
public class Main {
 
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

  也许有些朋友会说都会输出 false,或者也有朋友会说都会输出 true。但是事实上输出结果是:

true

false

   为什么会出现这样的结果?输出结果表明 i1 和 i2 指向的是同一个对象,而 i3 和 i4 指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是 Integer 的 valueOf 方法的具体实现:

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

  而其中 IntegerCache 类的实现为:

private static class IntegerCache {
        static final int high;
        static final Integer cache[];
        static {
            final int low = -128;
            // high value may be configured by property
            int h = 127;
            if (integerCacheHighPropValue != null) {
                // Use Long.decode here to avoid invoking methods that
                // require Integer's autoboxing cache to be initialized
                int i = Long.decode(integerCacheHighPropValue).intValue();
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - -low);
            }
            high = h;
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }
        private IntegerCache() {}
    }

  从这 2 段代码可以看出,在通过 valueOf 方法创建 Integer 对象的时候,如果数值在 [-128,127] 之间,便返回指向 IntegerCache.cache 中已经存在的对象的引用;否则创建一个新的 Integer 对象。

  上面的代码中 i1 和 i2 的数值为 100,因此会直接从 cache 中取已经存在的对象,所以 i1 和 i2 指向的是同一个对象,而 i3 和 i4 则是分别指向不同的对象。

  1. 下面这段代码的输出结果是什么?
public class Main {
    public static void main(String[] args) {  
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0; 
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
 
false
 
false

  Double 类的 valueOf 方法会采用与 Integer 类的 valueOf 方法不同的实现。在某个范围内的整型数值的个数是有限的,而浮点数却不是。

  注意,Integer、Short、Byte、Character、Long 这几个类的 valueOf 方法的实现是类似的。

     Double、Float 的 valueOf 方法的实现是类似的。

  1. 下面这段代码输出结果是什么:
public class Main {
 
    public static void main(String[] args) {        
 
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
 
true
 
true

  下面是 Boolean 的 valueOf 方法的具体实现:

public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

  而其中的 TRUE 和 FALSE 又是什么呢?在 Boolean 中定义了 2 个静态成员属性:

 public static final Boolean TRUE = new Boolean(true);
    /**
     * The<code>Boolean</code> object corresponding to the primitive
     * value <code>false</code>.
     */
    public static final Boolean FALSE = new Boolean(false);
  1. 谈谈 Integer i = new Integer(xxx) 和 Integer i =xxx; 这两种方式的区别。

  当然,这个题目属于比较宽泛类型的。主要有以下这两点区别:

  1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;

  2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况(注意这并不是绝对的)。

  1. 下面程序的输出结果是什么?
public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
 
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

需要注意的是:当 ” == ” 运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals 方法并不会进行类型转换。明白了这 2 点之后,上面的输出结果便一目了然:

true
false
true
true
true
false
true

  第一个和第二个输出结果没有什么疑问。第三句由于 a+b 包含了算术运算,因此会触发自动拆箱过程(会调用 intValue 方法),因此它们比较的是数值是否相等。而对于 c.equals(a+b) 会先触发自动拆箱过程,再触发自动装箱过程,也就是说 a+b,会先各自调用 intValue 方法,得到了加法运算后的数值之后,便调用 Integer.valueOf 方法,再进行 equals 比较。同理对于后面的也是这样,不过要注意倒数第二个和最后一个输出的结果(如果数值是 int 类型的,装箱过程调用的是 Integer.valueOf;如果是 long 类型的,装箱调用的 Long.valueOf 方法)。

         自动装箱、拆箱很酷很好,但实践中也要注意,避免无意中的装箱、拆箱行为,尤其是对性能敏感的场合,创建十万对象与十万整形开销差距不是一个量级的,在内存,对象头空间占用,处理速度上都有很大的差距。

参考资料:

链接:Autoboxing and Unboxing (The Java Tutorials> Lea…

链接:深入剖析 Java 中的装箱和拆箱 - 海 子 - 博客园

链接:Java 自动装箱与拆箱的实现原理 - 简书

极客时间