Java细节,自动封箱拆箱导致的NullPointerException问题

Java细节,自动封箱拆箱导致的NullPointerException问题

最近维护的安卓App碰到一个很神奇的bug,莫名其妙抛了个NullPointerException,真是让人头大,仔细研究,顺便探讨了一下,突然又觉得很有意思。

问题

问题是这样的,在返回一个类型是boolean的方法中,将从map里面取出的Boolean类型的值直接返回,代码一运行,执行到这立即出现了空指针问题(问题一)。

于是我就使用log分析,将字符和Boolean类型的值拼接打印,结果log处居然也出现了空指针问题(问题二)。

仔细想了想包装类型不就是对象么,取到了空值报异常不是很正常,改了改果然能运行了,可是仔细想想还是好神奇,简单探讨了一下,还挺有意思,这里写篇博客记录一下,案例代码如下:

代码

  • 问题一
private boolean getState(long id) {
		Map<Long, Boolean> stateMap= . . .;
		//问题一:此处报了NullPointerException
		return stateMap.get(id);
}
  • 问题二
private boolean getState(long id) {
		Map<Long, Boolean> stateMap= . . .;
		//问题二:此处也报了NullPointerException,并没有预期打印出null
		Log.d("TAG", "神马问题? state = " + stateMap.get(id))
		return stateMap.get(id);
}

简单分析

问题一

在第一段代码中,直接返回了stateMap.get(id),这里拿到的值实际是Boolean类型的null值,作为boolean类型返回需要进行拆箱,而null值拆箱,问题便不言而喻了。稍微修改一下,如下:

private boolean getState(long id) {
		Map<Long, Boolean> stateMap= . . .;
		//注意如果Boolean值为null的时候,自动拆箱会抛空指针异常
		Boolean state = stateMap.get(id);
		return state == null ? false : state;
}

因为Boolean类型是一个类,所以Boolean类型对象的默认值是null,我这直接当boolean使用明显不对,包装类型并不能完全代替基本类型

问题二

第二个问题才是本文最有意思的地方。前面说到了Boolean类型的值是一个对象,那我直接使用字符串拼接不应该拼接出null显示吗?如下:

private boolean getState(long id) {
		Map<Long, Boolean> stateMap= . . .;
		//问题二:此处也报了NullPointerException,并没有预期打印出null
		Log.d("TAG", "神马问题? state = " + stateMap.get(id))
		return stateMap.get(id);
}

神马问题? state = null

然而实际情况是抛了空指针异常,这又是什么问题?不急,往下看。

简单探讨

既然是拼接出了问题,首先了解一下字符串拼接的原理。Java使用 “+” 拼接字符串看起来像操作符重载,实际上并不是,Java是不支持运算符重载的,这其实只是Java提供的一个语法糖。反编译代码后会发现,其实调用的是StringBuilder的append方法,如下:

public class Main {
    public static void main(String[] args) {
        String s1 = "hello ";
        String s2 = "world!";
        String s3 = s2 + s1;
    }
}

编译,反编译上面的代码:

public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        String var1 = "hello";
        String var2 = " world!";
        (new StringBuilder()).append(var2).append(var1).toString();
    }
}

所以,实际上拼接会调用append方法,那看一下append方法:
在这里插入图片描述
这里有很多重载的方法,其中符合我们使用的有两个:

    //第一个,接收boolean类型
    @Override
    public StringBuilder append(boolean b) {
        super.append(b);
        return this;
    }

	//第二个,接收Object类型
    @Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }

那到底重载的时候选择哪个方法呢?这里其实很好理解,自动拆箱发生在什么地方?代码执行的时候发生在什么地方?理解清楚了,我们就知道到底会调用哪个方法了。

装箱和拆箱

下面是《Java核心技术 卷一》里面的原话,大概在5.4节(如果想看更多笔记,可以点这里)。

装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。

对于装箱和拆箱,实际上打开源码看一下就明白。以Boolean为例,装箱就是调用Boolean的valueOf方法选择预置的 TRUE or FALSE 对象(已经创建好的static对象),拆箱就是调用Boolean对象的booleanValue方法返回value。

问题二:结论

所以既然装箱和拆箱是编译器执行的,那毫无疑问,这里肯定是会执行appendboolean b)这个方法了。也就是说,Boolean类型的值在拼接时,首先要需要调用它的booleanValue()方法完成拆箱,然而和第一个问题一样,null对象怎么调用方法,自然抛出了空指针异常。

至此,完美撒花!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值