深入了解java的装箱与拆箱

1 前言

java是面向对象的语言,但是其也包含了8种基本数据类型,这8种数据类型不支持面向对象的编程机制,即不具备对象的特性,可以说是java语言早期设计的一种缺陷,为了解决8种基本数据类型的变量不能当成Object类型变量使用的问题,java提出了包装类(Wrapper class)的概念。本节主要了解:

  1. 基本类型与包装类型之间的转换
  2. 什么是装箱?什么是拆箱?
  3. 装箱和拆箱是如何实现的
  4. 装箱过程中的优化

2 正文

2.1 基本类型与包装类型之间的转换

下表格展示了8种基本类型对应的包装类型

基本数据类型 包装类 基本数据类型 包装类
byte Byte char Character
short Short float Float
int Integer Double double
long Long boolean Boolean

把基本类型转换成包装类型可以通过包装类的构造器来实现,比如

    int a = 5;
    // 通过构造器把基本整型变量包装成包装类型
    Integer aObj = new Integer(a);
    boolean b = true;
    // 通过构造器把基本布尔变量包装成包装类型
    Boolean bObj = new Boolean(b);

Note:

  • 如果传入构造器的类型不对,则会抛出java.lang.NumberFormatException
  • 当试图使用一个字符来创建Boolean对象时,比如传入的是true(可以忽略大小写),则会返回true对应的Boolean对象;如果传入其他字符(非false字符时也一样),则会创建false对应的Boolean对象,从源码中可以看出:
    public Boolean(String s) {
        this(parseBoolean(s));
    }

    public static boolean parseBoolean(String s) {
        return ((s != null) && s.equalsIgnoreCase("true"));
    }

当然,如果想将包装类型转换回基本类型,使用xxxValue()实例方法即可,比如

    Integer aObj = new Integer(5);
    // 取出aObj对象中的int变量
    int a = aObj.intValue();

    Boolean bObj = new Boolean("true");
    // 取出bObj对象中的boolean变量
    boolean b = bObj.booleanValue();

2.2 什么是装箱?什么是拆箱?

通过2.1可以了解到如何获取一个包装类。但是那样我觉得非常麻烦,因为基本类型的数值已经确定了,为什么还要通过构造器的方式来获取一个新的对象,不能直接通过赋值的方式吗?于是,jdk1.5就出了装箱和拆箱这两种概念。

    // 装箱
    Integer a = 5;
    // 拆箱
    int b = a;

装箱:通过指定的数值自动创建一个相对应的包装类型
拆箱:自动将包装类型转换为基本类型

2.3 装箱和拆箱是如何实现的

那么装箱和拆箱是如何实现的呢,使用javap -c Test指令对2.2的代码进行反编译,可以看到:

    iconst_5:把常量5从常量池中取出来并压入栈中
    invokestatic:调用Integer的static方法valueOf
    astore_1:从栈中弹出对象引用,然后将其存到位置为1的局部变量中,即Integer a = 5
    aload_1:将位置为1的对象引用局部变量压入栈,即将a引用压入栈中
    invokevirtual:调用实例方法intValue
    istore_2:从栈中弹出int类型值,然后将其存到位置为2的局部变量中

位置1,2是针对代码块来说的

    Integer a = 5;//1
    int b = a;//2

装箱的时候调用的是Integer.valueOf(int)方法,而在拆箱的时候调用的是Integer的intValue方法。并且可以看到,装箱的过程中,返回的是一个对象,这里产生了内存的消耗,会影响性能。

综上,装箱时调用的是包装器的valueOf方法实现的,而拆箱时调用的是xxxValue方法实现的

2.4 装箱过程中的优化

关于装箱或者拆箱,在面试时可能会遇到这样一道题:

    Integer ina = 2;
    Integer inb = 2;
    // 输出true
    System.out.println(ina == inb);

    Integer biga = 128;
    Integer bigb = 128;
    // 输出false
    System.out.println(biga == bigb);

为什么ina和inb经过装箱后,两者表明指向同一对象;而biga和bigb经过装箱后,指向的不是同一个对象呢?来看一下装箱的源码便知道了:

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

当传入的数值i在区间[low,high]内,便会从IntegerCache中获取;否则直接new一个新的Integer对象并返回。

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

可以看出来,IntegerCache中定义了一个Integer cache[]数组,当满足-128<=i<=127,就会放入数组中。在上述例子中,ina和inb在自动装箱的过程中调用了valueOf方法,由于数值在[-128,127]的范围内,所以会引用同一个对象;而对于biga和bigb来说,自动装箱的过程中同样会调用valueOf方法,由于不在范围内,所以创建了新的对象。

下面再来看一段程序:

     Double aDouble = 2.0d;
     Double bDouble = 2.0d;
     System.out.println(aDouble == bDouble);

结果输出的是false,查看源码Double.valueOf,发现并没有进行缓存。这是因为浮点数在一个区间内的数是无穷多个的,所以没有办法进行缓存。

再来看一段程序:


    Boolean aTrue = true;
    Boolean bTrue = true;
    System.out.println(aTrue == bTrue);

结果输出的是true,来看一下源码Boolean.valueOf:

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

发现布尔变量b直接与变量TRUEFALSE关联在一起,再来看下这两个变量:


    public static final Boolean TRUE = new Boolean(true);

    public static final Boolean FALSE = new Boolean(false);

这两个变量用static final修饰,即为全局变量,所有的Boolean实例共享。所以无论是什么Boolean(true或者false类型一致)对象,都会有共同的地址,用==判断当然为真

总结一下,哪些包装类有缓存且缓存范围是什么:

包装类 包装类 基本数据类型 包装类
Byte 全部缓存 Character c<=127
Short [-128,127] Float 没有缓存
Integer [-128,127] Double 没有缓存
Long [-128,127] Boolean 全部缓存

2.5 什么时候触发自动装箱和拆箱

平常我们使用时,这样就会触发自动装箱。

    Integer a = 5;

但是使用new Integer的方式则不会触发自动装箱

    Integer a = new Integer(5);

再来看一个例子:

    Integer a = 1;
    int b = 2;
    System.out.println(a == b);
    // output
    true

为什么会是true呢?应该不是同一个对象才对。因为在使用==进行比较时,触发了自动拆箱,此时==比较的是数值(因为都是基本类型)。只有当==两边都是包装类时,比较的才是对象的引用是否一致。

例子无穷多:

    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Long d = 3L;
    System.out.println(c.equals(a+b));
    System.out.println(d.equals(a+b));
    // output
    true
    false

a、b、c分别进行拆箱,a+b再进行+运算,再用equals进行比较。为什么会时两个不同的结果,来看下equals源码

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

可以看到,equals先判断类型是否为Integer,再判断值是否相等

最后一个例子:

    Integer a = 1;
    Integer b = 2;
    Long c = 3L;
    System.out.println(c == (a+b));
    // output
    true

a、b、c分别进行拆箱,a和b再进行+运算

    Integer a = 1;
    Integer b = 1;
    System.out.println(a == b);
    // output
    true

并不会触发自动拆箱!

以上可以总结为:

  • 当遇到表达式,且表达式包含运算符时,会进行自动拆箱
  • 当==两边都是包装类类型时,比较是否指向同一个对象;当==一边包含基本类型时,就会触发自动拆箱

3 总结

通过上述大概了解了java的如何把基本类型转换为包装类,如果自动进行装箱和拆箱以及装箱过程中需要注意的事项。

参考资料:

  1. 疯狂java讲义第二版–包装类型篇
  2. 深入剖析Java中的装箱和拆箱
  3. jdk1.8源码

猜你喜欢

转载自blog.csdn.net/qingtian_1993/article/details/81058949