作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
阶段4、深入jdk其余源码解析
阶段5、深入jvm源码解析
空指针,全名Null Pointer Exception,简称NPE,是Java程序员最熟悉的一个异常和错误。如果一个程序报错了,那么80%的概率是空指针。如何避免空指针,是Java程序员时刻需要考虑的一个问题。
虽然我们之前介绍了Optional来避免繁琐的空指针探测,但它并不是万能的:
这里主要想和大家聊聊第二种。
一般来说,能直接看到的空指针,大部分人都能避免。比如:
// 假设上文已经判断过,user不为null
if (user.getType().equals(CommonConstants.USER_TYPE)) {
// ...
}
此时大部分人都能意识到,即使user不为null,但user.getType()仍有可能为null,用userType去调用equals()就可能会抛NPE。较好的写法是:
// 假设上文已经判断过,user不为null
if (CommonConstants.USER_TYPE.equals(user.getType())) {
// ...
}
由于CommonConstants.USER_TYPE是常量,必然不为null,一个确定不为null的值调用equals()是不会发生NPE的,所以上面的代码是安全的。
但如果你的同事定义的常量是这样的:
public class CommonConstants {
// 他用了int,而不是Integer
public static final int USER_TYPE = 1;
}
大部人看到常量是int,基础类型无法调用equals(),而user.getType()又是不稳定的(可能为null),所以只能退而求其次,写成下面这样:
if (CommonConstants.USER_TYPE == user.getType()) {
// ...
}
乍一看:user不为null,所以user.getType()不会发生NPE,常量又是安全的,也没调用equals(),怎么看上面代码都不会出问题。
然而...
由于其他同事在代码的上游做了APP版本判断,对于旧版本是没有type值的。即使所有人都更新到最新版本,但路人甲还是旧版本,那么只要路人甲访问这个接口,type就为null。而我改造接口时并没有注意到这个细节,再加上刚好同事定义的type是int类型的,聪明反被聪明误,代码上线后收到了各种报警信息。
归根结底,对于
if (CommonConstants.USER_TYPE == user.getType()) {
// ...
}
它的厉害之处在于我们无法直接观察到空指针,因为光从代码层面看确实“很安全”,但问题发生在“拆箱”,中间隔了一个JDK语法糖,被蒙蔽了。当user.getType()发现自己要和左边的int值比较时,需要拆箱为基础类型。
总之,Integer拆箱底层会调用:intValue(),所以上面的代码编译后近似于:
if (CommonConstants.USER_TYPE == user.getType().intValue()) {
// ...
}
也就空指针了!
建议使用Objects.equals()等工具类封装的方法做等值比较:
if (Objects.equals(CommonConstants.USER_TYPE, user.getType())) {
// ...
}
为什么Objects.equals()能避免空指针呢?我们打开源码,发现一个不可思议的现象:
Objects.equals()内部竟然也用了==,于是一部分同学懵逼了,这和我们自己写的
if (CommonConstants.USER_TYPE == user.getType()) {
// ...
}
有啥区别?实际上,是有区别的。
一个int和一个Integer比较,要么int装箱为Integer,要么Integer拆箱为int,总之要在“同一个水平线”。Objects.equals()选择不改变Integer,而是把int装箱为Integer。int作为基础类型是没有null的,也就不会发生NPE。也就是说,Objects.equals()的思想是:用安全的装箱替代不稳定的拆箱。
部分同学可能考虑到了包装类缓冲池的问题,比如:
那么Objects.equals()能不能规避这种问题呢?
public class Test {
public static void main(String[] args) {
Integer wrapperValue = 128;
System.out.println(Objects.equals(CommonConstants.TYPE_WRAPPER, wrapperValue)); // true
Integer value1 = 128;
Integer value2 = 128;
System.out.println(value1 == value2); // false
}
static class CommonConstants {
public static final Integer TYPE_WRAPPER = 128;
}
}
你知道为什么两个结果不同吗?
参考答案:
public static boolean equals(Object a, Object b) {
return (a == b) // 即使因为包装类缓冲池导致 128 != 128,但后面还有另一个条件,一个为true即相等
|| (a != null && a.equals(b));
}
也就是说,==本身无法解决包装类缓冲池的问题,但Objects.equals()用了“或”逻辑,即使a==b翻车了,还能靠后面的(a != null && a.equals(b))力挽狂澜。
你说,是不是很细节?
对于一些常量,我还是觉得直接定义成Integer等包装类好一些,方便使用equals(),只要注意避免使用==即可。
除了相等比较,包装类和基本类型没啥区别。
我见过很多人习惯把枚举中的Integer code写成int code,其实真的不差这点内存...个人认为这个场景下使用int code弊大于利。比如个别场景客户端就是没有传code,而你在代码里调用getByCode(int code)就会导致null拆箱错误。