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对象怎么调用方法,自然抛出了空指针异常。
至此,完美撒花!