面试官:既然NPE是一个十亿美元的错误,那么你工作中是如何避免的?

无处不在的 NPE

有开发经验的人都知道 Java 中的空指针异常 NullPointerException(NPE),当我们试图使用一个值为 null 的对象引用时,就会抛出这个异常。

public class NpeDemo{    public static void main(String[] args){        NpeDemo npeDemo = null;        npeDemo.go();    }}

当执行上面的程序的时候,就会报NPE:

Exception in thread "main" java.lang.NullPointerExceptionat NpeTest.main(NpeTest.java:4)

像上面这样的简单方法,我们根据堆栈信息,很快就能找到问题所在。但是实际开发中,空指针可谓无处不在,一不小心就会踩坑,而且有些 NPE 是潜伏在程序当中的,只有遇到某些特殊情况时才会出现,很难测出来。

那么,我们要如何尽量避免 NPE 的发生呢?总结起来,有以下 7 条,值得收藏!

# 意识:调用方法时记得判空

这其实是句正确的废话!写 Java 的人都知道使用对象时有可能会出现空指针,所以 “最好都要记得” 判空,但关键是,如果都能记得的话,NPE 就不会那么难缠了。而且有些情况下,NPE 隐藏得很深,往往是在真正出问题的时候才会被发现。

提高判空意识,养成判空的直觉和习惯,肯定是最理想的解决方式。但是理想很性感,现实很骨感,人是不可靠的,因此我们可以借助工具来监督我们。

IDEA,就是一个非常不错的帮手,上面那段代码在 IDEA 的视角下,是这样的: 

像这种低级的错误,IDEA 会着重提醒你:此处有 NPE 出没,请格外小心!养成看警告的习惯比养成判空的习惯更容易些。

# equals 方法

我们经常需要判断两个对象是否相等(equals),此时,我们可能会这么做:

public String maybeNull(String word, String defaultVal) {    // don't do this! if (word.equals("haha")) {        return defaultVal;    } else if (word.equals(defaultVal)) {        return "Same";    } else {        return "OK";    } }

这个方法里,有两个地方容易出现 NPE,

word.equals("haha")

word.equals(defaultVal)

对于第一个,通常有两种处理办法:

word!=null&&word.equals("haha")

"haha".equals(word)

第二种显然更受欢迎,然而对于两个都是变量的 word.equals(defaultVal) 比对,第二种方法就不适用了,需要这样判断:

if ("haha".equals(word)) {    return defaultVal; } else if (word != null && word.equals(defaultVal)) {    return "Same"; }

想象一下,如果你的方法里有一堆这样的判断,会不会觉得烦,而且很容易走神遗漏几个判空?

所以我推荐使用 Objects.equals(a,b) 方法,这样无论两个对象是否有 null 值的存在,都不必担心:

if (Objects.equals("Null", word)) {    return defaultVal;} else if (Objects.equals(word, defaultVal)) {    return "Same";}

# 自动拆箱的陷阱

有一个不容易发现但是非常容易出现 NPE 的场景:

public boolean isZero(Integer num) {    return num == 0;}

这个方法,可能一眼很难发现其中的问题,而且正常使用的话也不会出问题。直到有一天,你把 null 值传给了这个方法,悲剧就发生了。

Exception in thread "main" java.lang.NullPointerException    at com.dadiyang.Computer.isZero(Computer.java:18)    at com.dadiyang.Computer.main(Computer.java:9)

这就是自动拆箱机制导致的异常,在做 == 比较的时候,包装类 Integer 对象会自动拆箱为 int,当这个 Integer 对象是 null 值时,拆箱就会抛出 NPE。

这种场景下,最好的方法当然是尽量将方法参数改为 int:

public boolean isZero(int num) {    return num == 0;}

但是如果你的方法调用者确实有要处理包装类的需求,那么,调用的人就得小心了,他们同样会遇到自动拆箱问题: 

这种情况下你的方法就得负责判空了。你也可以这样写:

public boolean isZero(Integer num) {  return Objects.equals(0, num);}

注意:只有你明确知道你的方法就是需要处理包装类的时候,才使用包装类做参数。

Objects 工具类为我们提供了几个相当不错的静态方法,使用起来可以大大降低 NPE 出现的概率。除了 Objects.equals 方法之外。

Objects.toString(Object o, String nullDefault)Objects.requireNonNull(T obj, Supplier<String> messageSupplier)

也是非常好用的方法!

# 判断字符串是否为空

实践中,对字符串进行判空是非常高频的操作。我们不仅要判断一个字符串是否为 null,而且还要知道它是否为空串,甚至还需要判断它是否为空白字符串。

新人容易犯的错误就是上来就直接判断:

if(str.trim().isEmpty())

NPE 就此产生! 这种场景下,一定要记得 str 有可能为空,所以正确的姿势应该是:

if(str==null||str.trim().isEmpty())

然而,还是想象一下,无处不在的这种散发着臭味的无聊判空,写着写着就会打起瞌睡来,然后遗漏掉一两个。还是那句话,人是不可靠的!

其实 commons-lang3 为我们提供了一些非常好用的方法,最常用的,就是 StringUtils.isEmpty/isBlank(str) 这两个用于判断字符串是否为空的方法了。无需担心 str 为空,尽管使用就可以了:

if(StringUtils.isBlank(str))

另一个类似的方法就是字符串的大小写转换了:

if(word.equals(str.toLowerCase()))

如果你经常这么写的话,肯定天天被 NPE 缠身。这样的表达式,稍微思考一下,你可能会知道进行判空:

if (word != null && word.equals(str != null ? str.toLowerCase() : null))

OK,如果你这么写,至少不会报空指针了,然而,一个坑也就挖好了!可能几个月后的某一天,一个让你死活查不出来的 Bug,让你通宵一整晚!就因为你没有考虑到 word 和 str 都为 null 值的情况。所以正确的写法是:

if (word == null && str == null) {  return true;} else {  String lowerStr = str == null ? null : str.toLowerCase();  return word != null && word.equals(lowerStr);}

好吧,这样无聊的代码,我实在编不下去了~~

于是小手一挥,我写下:

if (Objects.equals(word, StringUtils.lowerCase(str))){...}

# 检查集合是否为空

跟字符串判空一样,集合判空也是很常见的语句:

if(!list.isEmpty()){}

说了那么多,你还是这么写,NPE 估计爱死你了~

if(list!=null&&!list.isEmpty()){}

能不能更优雅些呢?Of course!

commons-lang3 的好哥们 commons-collections 为我们提供了对集合的一系列工具方法,其中就有判空的方法 CollectionUtils.isEmpty(list),因此,更优雅的写法是:

if(CollectionUtils.isNotEmpty(list)){}

有 isEmpty 当然就有 isNotEmpty 咯~

# 写方法时尽量不要返回 null 值

我们经常会定义一个返回一个集合的方法,像这样:

public List<String> asList(String[] arr){  if (arr == null) {    return null;  }

return Arrays.asList(arr);}

这样的方法定义,当然是没问题。但是如果调用你的方法的人是个大马哈,就会比较抓狂了。咱们做开发,还是要有点用户思维比较好。为了尽量避免给用户(其实是我们自己)带来不必要的麻烦,写方法时尽量不要返回 null 值,特别是当返回值是集合时,可以通过返回空集合来避免空指针。当然啦,总是 new 出一个空集合来,也是很影响性能的,因此我们可以使用: Collections.emptyMap/emptySet/emptyList(); 来返回一个全局共享的不可变的空集合:

public List<String> asList(String[] arr){

if (arr == null) {    return Collections.emptyList();  }

return Arrays.asList(arr);}

注意咯,Collections.emptyList() 返回的是一个不可变的空集合,不要妄想往里添加元素,否则会报错的!绝大多数情况下,我们不可能直接往方法返回的集合里添加元素的,所以放心使用就好。

作为调用者,我们需要采取 “不信任” 的原则,就算这个方法声称不会返回 null 值,我们在处理返回值时仍要考虑 null 值。鬼知道哪天会不会一个不小心有人把这个方法给改了,突然返回一个 null 值呢~

# Optional

Optional 是 Java8 带来的新特性,它还算挺不错的,但是相较于其他语言,如 Grovvy 或 C# 对空指针的处理,这个 Optional 我感觉真是弱爆了。Grovvy 和 C# 中,有一个 .? 操作符,你只要 str?.toString() 就可以达到。

str==null?null:str.toString()

相同的效果。但是 Java 则不行。

不过,毕竟 Optional 是 Java8 推出的解决 NPE 的利器,我们还是有必要学习一下的。特别是当 Optional 结合 Lambda 表达式的场景下会非常强大。具体可以参考这篇文章:http://weishu.me/2015/12/08/use-optional-avoid-nullpointexception/

假设我们有这样的代码:

if ("3.0".equals(soundcard.getUSB().getVersion())) {  System.out.println("ok");}

显然,这里 soundcard 和 soundcard.getUSB() 都有可能为空,所以要进行判空:

if(soundcard != null){

  USB usb = soundcard.getUSB();

if(usb != null && "3.0".equals(usb.getVersion()){    System.out.println("ok");  }}

使用 Optional 和 Lambda 表达式,可以免去这些判空:

Optional.ofNullable(soundCard)  .map(SoundCard::getUsb)  .filter(usb -> "3.0".equals(usb.getVersion()))  .ifPresent((usb) -> System.out.println("OK"));

这样,当 soundCard 为空,或者 soundCard.getUsb() 为空时, 什么事情都不会发生。只有当它们都不为空,而且 usb.getVersion() 为 3.0 时,才会打印 OK。

# 总结

1.意识:使用 obj.doSomething() 时记得判断 obj != null。意识的养成需要一个漫长的过程,我们可以通过工具来帮忙,IDEA 就是一个非常出色的工具。

2.判断对象是否相等时,使用 Objects.equals(a, b) ,当然 Objects 工具类还贴心地为我们提供了 toString 和 requireNonNull 这样的好帮手

3.自动拆箱的陷阱。当使用包装类与原始类型做比对时,要特别注意空指针问题

4.检查字符串是否为空时,使用 commons-lang3 包 StringUtils 提供的isEmpty 和isBlank 方法 。另外, 使用 lowerCase 和 upperCase 进行字符串转换大小写转换,也可以避免空指针

5.使用 commons-collections 包的 CollectionUtils.isEmpty 方法来检查集合是否为空

6.返回集合的接口若需要返回空,则返回空集而不是 null。但是每次都 new 出新的集合,会影响性能和不必要的对象创建,使用 Collections.emptyList(); 可以返回全局共享的不可变空集合

7.Optional 是 Java8 推出的解决 NPE 的利器,当它跟 Lambda 表达式结合时会非常强大。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • pyspark.sql模块 模块上下文 Spark SQL和DataFrames的重要类: pyspark.sql...
    mpro阅读 9,392评论 0 13
  • 一、编程规约 (一)命名规约 【强制】 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。反...
    喝咖啡的蚂蚁阅读 1,444评论 0 2
  • 阿里巴巴 JAVA 开发手册 1 / 32 Java 开发手册 版本号 制定团队 更新日期 备 注 1.0.0 阿...
    糖宝_阅读 7,344评论 0 5
  • 1.HashMap是一个数组+链表/红黑树的结构,数组的下标在HashMap中称为Bucket值,每个数组项对应的...
    谁在烽烟彼岸阅读 994评论 2 2
  • 昨天他叮嘱我早上六点给他电话叫他起床,六点多我打了过去。 电话里他神志不清的一直“嗯嗯嗯”。 心里想象着他萌萌的样...
    弓长口十阅读 171评论 0 0