Skip to main content

我患上了空指针后遗症

· 7 min read
古时的风筝

下面这个报错,相信没有任何一个 Java 程序员没有被它折磨过。我们对他的熟悉程度简直超过了 Hello World。 何止是熟悉,那简直是深恶痛绝,以至于我对它都产生了后遗症。

每当本地调试出现这个错误的时候,都恨不得掐一下大腿,然后默默的对自己说:垃圾,还犯这么愚蠢的错误呢?

不知道有多少同学和我一样有这种感受呢?

回想起我之前接手的一个项目,线上出现了问题,当我到了服务器一看日志,只有几个单词,那就是 java.lang.NullPointerException,那一刻我是头晕目眩,差点一头撞在 27 寸的显示器上。回想上一次出现这种症状,还是几年前挤早高峰的公交车,挤的我双脚离地,外加有点低血糖。

当然主要问题并不是 NPE(NullPointerException),还是要仰仗前辈异常处理的“非常优秀”,异常包裹的严严实实的,只留了java.lang.NullPointerException这一点点信息。

于是只能打开代码,找到报错的接口,一步步排查,满眼看去,皆可空指针啊。从此之后,空指针异常给我留下了深深的阴影。

好在从 JDK 14之后,NPE 异常不再仅仅是简单的这几个单词了,而会附带更加具体的异常信息,比如对一个赋值为 null 的字符串求长度,能捕捉到下面这样的异常信息:

Cannot invoke "String.length()" because "s" is null

空指针的由来

要说空指针异常,那还不只是 Java 的问题,绝大多数语言都有这个问题,比如 C++、C#、Go,但是也有没有这个问题,比如 Rust 。

空指针最早是编程界的鼻祖级人物 Tony Hoare 引入的,早在 1965年,他设计 ALGOL 60 语言的时候引入了Null 的设计,ALGOL 可谓是 C 语言的祖宗。ALGOL 中的 Null 被后来的众多语言设计者引入,就包括前面提到的这些语言。

Tony Hoare 不仅发明了我们熟悉的 Null,还是令众多算法残废闻风丧胆的快速排序算法(Quick Sort)的发明者,这个算法也是当前世界上使用最广泛的算法之一。

空指针后遗症.png

2009年3月他在Qcon技术会议上发表了题为「Null引用:代价十亿美元的错误」的演讲,回忆自己1965年设计第一个全面的类型系统时,未能抵御住诱惑,加入了Null引用,仅仅是因为实现起来非常容易。它后来成为许多程序设计语言的标准特性,导致了数不清的错误、漏洞和系统崩溃,可能在之后40年中造成了十亿美元的损失

如何应对空指针

处理空指针有一些措施,我们常常称之为「防御式编程」,这个说法也很形象,你不防着它,它真的就上来伤害你。

**1、**主动检查空指针,要使用一个变量之前,要检查这个变量是不是空,不是空再操作,比如常用的对字符串判空。

public static boolean isEmpty(CharSequence cs) {
return cs == null || cs.length() == 0;
}

对应的很多字符串工具类都有 isEmptyisNotEmptyisNotBlank 这种方法。

同样的,还有对于集合的判断,好多工具包都有 CollectionUtil.isEmpty这样的方法。

为了避免空引用异常,有时候我们写的代码可能想下面这个样子,一步一判空。这样可以提高代码的健壮性和可靠性,但是看上去并不是很美观。

public static String getUserOrderDetail(Integer userId) {
User user = User.getUser(userId);
if (user != null) {
Order order = user.getOrder();
if (order != null) {
Address address = order.getAddress();
if (address != null) {
String detail = address.getDetail();
if (detail != null) {
return detail;
}
}
}
}
return "不好意思,找了半天,没找到";
}

还好,Java 8 中引入的 Optional 类可以简化这个流程。

public static String getUserOrderDetail(Integer userId) {
return Optional.ofNullable(User.getUser(userId))
.map(User::getOrder)
.map(Order::getAddress)
.map(Address::getDetail)
.orElse("不好意思,找了半天,没找到");
}

2、 能不返回 NULL 的话,就尽量不返回 NULL

比如有些获取集合的方法,没有结果的话,可以返回一个空列表。这种方式对于提供给前端或者消费者使用的接口更加适用,返回一个空集合要远比返回一个空更友好。

3、 能抛异常的话,宁可抛异常,也不要返回 NULL

还有一些情况,抛出给调用者一个具体的异常,要比返回一个 NULL 更加能让调用者清楚到底发生了什么。

比如根据一个用户的信息,但是发现用户不存在了,直接返回给调用者一个「用户不存在」的异常信息更明确,而不是返回一个 NULL,让调用方去猜。

风筝

作者

风筝

古时的风筝,一个平庸的程序员,主语言 Java,第二语言 Python,其实学 Python 的时间比 Java 还要早。喜欢写博客,写博客的过程能加深自己对一个知识点的理解,同时还可以分享给他人。喜欢做一些小东西,所以也会一些前端的东西,React、JavaScript、CSS 都会一些,做一些小工具还够用。