万恶的空指针

发布时间:2023年12月28日

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

事故场景

空指针,全名Null Pointer Exception,简称NPE,是Java程序员最熟悉的一个异常和错误。如果一个程序报错了,那么80%的概率是空指针。如何避免空指针,是Java程序员时刻需要考虑的一个问题。

虽然我们之前介绍了Optional来避免繁琐的空指针探测,但它并不是万能的:

  • 有时候,某些场景使用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(),只要注意避免使用==即可。

  • 相等:equals
  • 大于:>
  • 小于:<

除了相等比较,包装类和基本类型没啥区别。

我见过很多人习惯把枚举中的Integer code写成int code,其实真的不差这点内存...个人认为这个场景下使用int code弊大于利。比如个别场景客户端就是没有传code,而你在代码里调用getByCode(int code)就会导致null拆箱错误。

文章来源:https://blog.csdn.net/smart_an/article/details/135259849
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。