Java深坑——为什么2021==2021是false?深入探讨Java的自动装箱/拆箱(autoboxing/unboxing)的来龙去脉

一道看似简单的题目

今天看到一个题目,挺有意思的,题目很简单,看看你会不会?

下面一段java代码里的func()输出结果是什么?

public class Autoboxing {
	public void func() {
		Integer a = 2021;
		Integer b = 2021;
		System.out.println(a == b);
	}
}

运行结果是false,可能有点出乎意料。我们改改,把a和b的值变成1:

public class Autoboxing {
	public void func() {
		Integer a = 1;
		Integer b = 1;
		System.out.println(a == b);
	}
}

结果是true!更出乎意料了吧?如果你感到迷惑甚至惊讶,不妨花几分钟来看看本文。

Java的装箱、拆箱行为

Java对基本类型都有一个相应的包装类,如Integer/int、Float/float、Boolean/boolean、Byte/byte等。Java 1.5开始自动装箱、拆箱,在恰当的时候包装类会被转为基本类型,基本类型也能够被自动转换成包装类参与代码的执行。举个例子:

Integer integer = 10; // 装箱,整数10会被自动包装成Integer对象
int num = integer; // 拆箱,Integer对象integer会被转换成整数赋值给num

其他类型Boolean、Character、Byte、Short、Long、Float、Double也都有类似的行为。有兴趣的朋友可以试试。

深入探索——实现、答案和实战

看了上面装箱、拆箱的例子,貌似很简单啊。不过,这还是没有解释开头那段代码诡异的输出结果是怎么来的。接下来我们抽丝剥茧,从3个方面逐步深入了解其中的原因:

1. 装箱、拆箱的实现

我们先来看看上面一段代码部分java字节码。其中a、b赋值前都触发了装箱操作,通过Integer.valueOf(int)返回Integer对象。自动装箱是通过调用各包装类的valueOf()来实现的。

public void func();
  Code:
     0: sipush        2021
     3: invokestatic  #15                 // 装箱,转化成Integer对象。Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     6: astore_1
     7: sipush        2021
    10: invokestatic  #15                 // 装箱,转化成Integer对象。Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    13: astore_2
    14: getstatic     #21                 // Field 

我们对func()稍作修改,做个加法。可以看到,拆箱的过程是调用了Integer.intValue()。其他包装类的实现也类似,都是调用相应的xxxValue()来实现的。

原始代码:
    public void func() {
	    Integer a = 2021;
    	Integer b = 2021;
	    int c = a + b;
    }

字节码:
  public void func();
    Code:
       0: sipush        2021
       3: invokestatic  #15                 // 装箱,Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       6: astore_1
       7: sipush        2021
      10: invokestatic  #15                 // 装箱,Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: astore_2
      14: aload_1
      15: invokevirtual #21                 // 拆箱,Method java/lang/Integer.intValue:()I
      18: aload_2
      19: invokevirtual #21                 // 拆箱,Method java/lang/Integer.intValue:()I
      22: iadd
      23: istore_3
      24: return

2. 谜题的解开

实现方式也是简单明了,但前面的问题还是没有答案。别急,这个问题还是稍微有点复杂的。表达式(a == b)是在比较2个对象是否为同一个对象,并不会调用拆箱的过程。那么a和b为什么有时相同有时又不同呢?让我们以Integer为例来具体看看装箱是怎么操作的。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high) // 默认IntegerCache.cache里缓存了256个Integer对象[-128,127]
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }


// 以下是IntegerCache
    private static class IntegerCache {
        static final int low = -128; // 下限
        static final int high; // 上限
        static final Integer cache[]; // 缓存的Integer对象,大小为(high - low -1)

        static {
            // 缓存上限可以通过property配置
            // 为了清除看明白核心逻辑,我删除了读取、使用property的部分代码
            int h = 127;
            ...
            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() {}
    }

Integer内部缓存了部分Integer对象(默认是-128到127),如果参数i的值落在这个区间里,那么就返回同一个对象。所以,a、b都是1的情况下,返回的是同一个Integer对象;2021的情况下,则调用new Integer(i)创建了2个新的对象。为什么会有这样的缓存呢?原来是java语言规范(JLS)的要求,Oracle网站上原文是这样的:

If the value p being boxed is truefalse, a byte, or a char in the range \u0000 to \u007f, or an int or short number between -128 and 127 (inclusive), then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.

大致意思是说,如果一个基本类型p(比如100)的值处于某取值区间(如-128到127),该值p装箱后的对象分别为r1和r2。那么,表达式r1==r2永远为true。问题来了,那为JLS是处于什么考虑制定了这样的规范呢?

This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.

原来是出于性能方面的考虑。估计是因为java最初名为green,是为了机顶盒等资源有限的设备设计的,目前也广泛应用在一些嵌入式设备上。这样的设计可以在一定程度上提升执行效率。而且JLS鼓励实现者内存充裕的情况下缓存更大范围的装箱对象,可以缓存所有char、short值,甚至将int、long的缓存范围增加到-32K到32K。

不过请注意,Double、Float是没有这样的缓存机制的。原因很简单,[-128,127]区间内的浮点数太多了,理论上是无限的,自然没法全部缓存;缓存一部分又意义不大。

3. 装箱、拆箱的实战

那么什么时候会触发装箱、拆箱呢?我们先来看一段代码,你能说出它的运行结果吗?

	public void func() {
		Integer a = 1;
		Integer b = 2;
		Integer c = 3;
		Long d = 3L;
		System.out.println(c == (a + b));
		System.out.println(c.equals(a + b));
		System.out.println(d == (a + b));
		System.out.println(c.equals(a + b));
		System.out.println(d.equals(a + b));
	}

第一行,a+b包含算术运算,触发拆箱,调用Integer.intValue()获取int值,做加法得到结果3。然后,“==”触发拆箱,Integer.intValue(c)的值为3,表达值为true。

第二行,前面步骤与第一行相同,只是调用了Integer.equals(Object obj)。传入参数是Object,触发装箱,然后比较数值。这里要看一下equals的实现。该方法先是判断了传入对象的类型,然后再考虑比较值。看到这里,最后2行的输出结果也就不难理解了。

    public boolean equals(Object obj) { // 传入参数是Object,对于基本类型会触发自动装箱
        if (obj instanceof Integer) { // 先检测类型,如果类型不同,则直接返回false
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

第三行,d是Long类型,a+b的结果是int,做比较的时候会触发拆箱,并将a+b的结果与d的值进行比较。

第四行,结果比较好理解。这个主要是用来跟第五行对比的。

第五行,d是Long,a+b结果是int。a+b先被装箱成为Integer,Long.equals()也先会检查传入参数的类型是不是Long,发现不是,则直接返回false,没有去比较数值。这与Integer的源码是类似的。

总结一下:算术运算会触发拆箱;不同类型equals()返回false;一段范围内的数字装箱后返回的包装类有缓存

异常处理

有一个小问题需要注意,触发拆箱的时候会调用xxxValue()。如果此时包装类对象是null,会产生NPE。所以,利用这个机制要小心,还是少用为好。

		Integer a = null;
		System.out.println(a == 100); // 触发拆箱,但a是null,会抛出NPE

为什么要有自动装箱呢?

泛型与容器。

要存储一个对象,要么存放一个该对象的引用;要么为它开辟一块内存,存放一份拷贝。前者可以用reference、指针实现,存储的数据是定长的;后者则需要知道该对象的大小才可以。对于java来说,容器中保存的不是对象本身,而是指向对象的引用,每个引用的大小是相同的。而基本类型(如int、short、double)所占内存却不一样,因而基本类型没法放到容器中。通过包装类把基本类型包装成“对象”,就可以方便地使用各种容器了。容器在运行期其实操作的都是Object,即便通过泛型技术指定了类型,如:ArrayList<String> list = new ArrayList<>(),ArrayList内部也不会创建String数组的,这就是所谓的泛型擦除。有兴趣的同学可以看看ArrayList的实现。

好了,关于自动装箱、拆箱,今天就聊到这里。欢迎大家留言交流:)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值