浅聊Java中的自动装箱和拆箱

引言

前些天,一个朋友问了我如下代码的执行结果是什么,

public class Main {
    public static void main(String[] args) {
        Integer i1 = null;
        System.out.println(i1 == 1);
    }
}

我说会NPE(NullPointerException)空指针异常,虽然的确执行后会报空指针异常,但是对于空指针的根本原因,我那时候还答不上来。这几天抽空看了一下相关的资料并且自己写代码Debug了一下,将自己对Java自动装箱和拆箱的理解整理成此文。

基本数据类型

Java中一共有8种基本数据类型(Primitive Type),其中有4种整型,2种浮点型,1种用于表示Unicode编码的字符单元的字符类型char和一种用于表示真值的boolean类型。其中整型和浮点型如下:
整型

类型储存需求取值范围
int4字节-2 147 483 648 ~ 2 147 483 647
short2字节-32 768 ~ 32 767
long8字节- 9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807
byte1字节-128 ~ 127

浮点类型

类型储存需求取值范围
float4字节大约± 3.402 823 47E+38F
double8字节大约±1.797 693 134 862 315 70E+308

自动装箱和拆箱

很多时候我们会遇到需要将int这样的基本类型转换为对象,如下:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(3);
    }
}

此时ArrayList对象的add方法需要传入一个Integer对象,为什么我们传入一个int类型的数值也能够正常执行呢?这里就涉及到Java中的自动装箱和拆箱。
在Java中,所有的基本数据类型都有一个与之对应的类。例如,Integer对应基本类型int。通常,我们称这些类为包装器(Wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean。
在上述代码中,当我们调用**list.add(3)**时,将自动地变换成,list.add(Integer.valueOf(3)),这种变换被称为自动装箱,即基本数据类型被自动打包成其对应的包装器类型。
同样,相反地,当将一个Integer对象赋值给一个int值时,例如:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(3);
        int index = 1;
        int i = list.get(index);
    }
}

将会自动地拆箱,也就是说,编译器int i = list.get(index),翻译成,int i = list.get(index).intValue()
甚至在算术表达式中也能够自动地装箱和拆箱。例如,可以将自增操作符应用于一个包装器引用:

public class Main {
    public static void main(String[] args) {
        Integer i = 1;
        i++;
    }
}

编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,最后再将结果装箱。

关于"=="运算符

作为运算符的一种,"=="既可以应用于包装器类型对象,也可以用于基本数据类型值,还可以用于两者之间的比较,如下:

public class Main {
    public static void main(String[] args) {
        Integer i1 = 27;
        Integer i2 = 27;
        // 包装器类型之间进行比较,比较的是对象引用i1和对象引用i2的地址
        System.out.println("i1 == i2" + ", " + (i1 == i2));

        int i3 = 27;
        int i4 = 27;
        // 基本数据类型(数值型)之间进行比较,比较的是i3和i4所存储的数值
        System.out.println("i3 == i4" + ", " + (i3 == i4));
        
        // 数值类型和对应包装器类型之间进行比较,包装器类型对象会先自动拆箱为基本数据类型再进行比较。
        // 所以本质上也是两个基本数据类型之间进行比较
        System.out.println("i1 == i3" + ", " + (i1 == i3));

        Integer i5 = 200;
        Integer i6 = 200;
        int i7 = 200;
        System.out.println("i5 == i6" + ", " + (i5 == i6));
        System.out.println("i5 == i7" + ", " + (i5 == i7));
    }
}

如代码中的注释所解释,当"=="运算符两边为包装器类型时,比较的是存储两个对象引用的地址是否相等,当符号两边是基本数据类型时,比较的是两个基本数据的值是否相等。当符号一边是包装器类型,另一边是基本数据类型时,包装器类型对象会自动拆箱为基本数据类型值,再进行比较,本质上也是两个基本数据类型之间进行比较。
上述代码的执行结果如下:
执行结果
从执行结果上来看,包装器类型对象i1和i2,i5和i6的比较为什么会出现截然相反的结果?按照前面的说法,当符号两边都是包装器类型时,比较的是存储两个对象引用的地址是否相等。那表明i1和i2是同一个Integer类型引用,i5和i6是不同的Integer类型引用。Why?Let’s check the code.
从前面自动拆箱的内容来看,当执行到Integer i1 = 27时,相当于执行了,Integer i1 = Integer.valueOf(27),Integer.valueOf(int i)代码如下:

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

可以看到,当i的值在IntegerCache.low和IntegerCache.high之间时,我们返回的是IntegerCache.cache数组对象中存储的值,而IntegerCache.low和IntegerCache.high的具体值如下:

    private static class IntegerCache {
    	// low的值为-128
        static final int low = -128;
        // high的值我们可以通过"java.lang.Integer.IntegerCache.high"属性来配置
        // 默认为127
        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.low和IntegerCache.high的默认值分别为-128和127,虽然IntegerCache.high可以通过JVM虚拟机配置进行修改,但是我反正从来是没改过。因此,当对[-128, 127]之间的基本数据类型值进行自动装箱(也就是Integer.valueOf(int i))时,返回的是缓存区中所存储的固定对象,我觉得是Java开发者在设计时,为了避免对常用的基本数据类型的频繁创建导致开销过大,因此设计了这样的逻辑。

注:自动装箱规范要求 boolean、byte、char ≤ 127,介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。

这样再去看为什么i1 == i2会是true,因为i1和i2实际上都是缓存区中同一个对象引用,如下图:
i1和i2实际对象引用
而为什么i5 == i6会是false,因为i5和i6是不同的对象引用,如下图:
i5和i6实际对象引用

关于一开始的NPE

还记的一开始我说的我朋友问我的代码段吗?

public class Main {
    public static void main(String[] args) {
        Integer i1 = null;
        System.out.println(i1 == 1);
    }
}

为什么这里会报空指针异常?当"=="运算符两端分别是包装器类型和基本数据类型时,包装器类型会自动拆箱为基本数据类型,再进行比较。因此,这里面其实相当于还执行了一次i1.intValue(),当执行到这里时,便出现NullPointerException了。
可以看到我在前文中,对编译器进行了加粗处理,因为自动装箱和自动拆箱实在编译阶段就进行的,编译器会自动的为我们加上诸如,Integer.valueOf(int i)和intValue()这样的装箱和拆箱方法。
参考前面的注释,对如下代码的执行结果,是否在意料之中?

public class Main {
    public static void main(String[] args) {
        Byte bt1 = 127;
        Byte bt2 = null;
        byte bt3 = 127;
        // 不同对象引用
        System.out.println("bt1 == bt2" + ", " + (bt1 == bt2));
        // 自动拆箱后比较
        System.out.println("bt1 == bt3" + ", " + (bt1 == bt3));


        Boolean b1 = true;
        Boolean b2 = null;
        boolean b3 = true;
        // 不同对象引用
        System.out.println("b1 == b2" + ", " + (b1 == b2));
        // 自动拆箱后比较
        System.out.println("b1 == b3" + ", " + (b1 == b3));


        Character ch1 = 'a';
        Character ch2 = 'a';
        char ch3 = 'a';
        Character ch4 = null;
        // char小于127包装成同一对象,相同对象引用
        System.out.println("ch1 == ch2" + ", " + (ch1 == ch2));
        // 自动拆箱后比较
        System.out.println("ch1 == ch3" + ", " + (ch1 == ch3));

        // 下面都会报NullPointerException
        System.out.println("bt2 == bt3" + ", " + (bt2 == bt3));
        System.out.println("b2 == b3" + ", " + (b2 == b3));
        System.out.println("ch3 == ch4" + ", " + (ch3 == ch4));

    }
}

执行结果如下:
执行结果

最后

对于非基本数据类型String,如下的代码执行结果,又该如何解释呢?

public class Main {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "a";
        String s3 = "abcd";
        String s4 = "abcd";
        String s5 = null;
        System.out.println("s1 == s2" + ", " + (s1 == s2));
        System.out.println("s3 == s4" + ", " + (s3 == s4));
        System.out.println("s1 == s5" + ", " + (s1 == s5));
    }
}

执行结果,
执行结果
这就涉及到Java中字符串常量池相关的概念了,等我有空再写一篇来剖析一下吧!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值