NPE(NullPointerException) 是在Java开发中常遇到的问题,特别对于刚入门的Java开发者来说,很容易忽略空指针的问题,进而影响整理代码质量。自己查阅了网络上相关资料,在这里对NPE空指针问题的解决方法做一个总结。
NPE的应对,整体上分为两大情况——1,接受外部数据,处理这些外部数据时,出现NPE;2,自己定义方法/接口时,空指针作为返回结果。
接受外部数据时避免出现NPE
有时候我们会将自己的代码封装成包或者服务,以供他人调用。外界调用我们代码时候传入的参数可能会不符合我们的预期,此时如果我们不对其进行检验,就有可能出现NPE的问题:例如,传入参数为空值,或者传入的请求没有某个字段。这种情况下,我们务必做好入参合法性检验,避免NPE报错的出现。以下是外部数据空指针检查处理的一些经验。
对方法入参进行 null 空值判断以及报错
防止 null 值入参对我们代码程序的影响,最直接的方式就是对入参进行 null 判断,对不符合预期的 null 值进行特殊处理(抛出异常Exception,输出相应日志等)。
我们可以使用Objects.requireNonNull(x)
进行空值检查,当参数为 null,则会抛出异常。我们也可以在方法声明时,在参数前加入@NotNull
修饰符,例如public void foo(@NotNull int x)
。这样在进行开发时,IdeaJ 会对明显的空值输入提示,从而减少NPE发生可能。进一步,我们可以使用 lombok 包中的 @NonNull
修饰符同时完成上述两点,和 @NotNull 一样用法,在编译时 lombok 会在方法开头自动生成空值检测的代码,此外 IdeaJ 也会有相应的入参空值提醒。
需要注意的是,我们的代码作为模块被别人开发调用时,入参空值报错是可行的,但是不是所有情况抛出异常都是合适的,如果我们的代码是线上作为服务被别人调用,我们可以进行日志输出和相应的空值逻辑处理,而非简单的抛出异常,以保证我们线上服务状态的正常。
对get方法返回的值多加小心
常常我们需要从某个数据对象获取某个字段值,比如从一个pojo类中get某个字段,从map中拿到key对应的value,或者是从上游请求传来的jsonobject中获取某个字段值。有时候疏忽了这些数据对象隐含的null值可能性,会导致NPE问题。例如,Map类的get方法在key不存在时,会返回null;同样的,fastjson中JSONObect类的get相关方法(e.g. getString, getArray)也会在给定字段不存在时返回null。
一个简单的经验准则,当方法名有get时,有意识考虑这个get出来的值会不会是空值null,会不会导致空指针的问题。
判断null
直接判断null的写法
在进行 null 判断的时候,我经常看到这样的写法 if(null == yourObj)
,为什么要把null写在判断表达式的前方?我在网上查阅了一下相关资料,这样写的原因主要是防止在 if 语句中相等判断运算符==
误写成赋值运算符=
,在Java 1.5以前,如果将if(yourObj == null)
写成 if(yourObj = null)
在编译时,是不会报错的。虽然在Java 1.5以后 if语句要求表达式值为布尔 boolean 值,但if(null == yourObj)
的代码习惯一直延续下来。
此外,如果你对代码可读性要求更高,我们可以用 Objects.nonNull()
(或者Objects.isNull()
)来判断一个对象是否为空值。
连续判断null情况下一种简洁写法
在解析json时,如果要取的字段是多层级嵌套在里层的,会遇到连续判断空指针的情况,如果在if里面嵌套if语句会影响代码可读性,一种办法是使用Optional解决这个问题。具体的例子如下:
|
|
一些空值判断的 Helper 方法
当我们在判断字符串类型 String 变量为空时候,有时候null值和空字符串“”会用相同的业务逻辑去处理,我们可以利用 org.apache.commons.lang3
中辅助类StringUtils
的isEmpty()
进行字符串为“空”的判断,该方法在字符串为null值或空字符串时都会返回true。更进一步的,StringUtils
的isBlank()
在上面两种情况外,在字符串只有空格的情况也返回true。
类似的,在判断集合类为空时候,null值与空集合可能有相同的处理逻辑,我们也可以利用org.apache.commons.lang3
中辅助类 CollectionUtils
的isEmpty()
同时判断变量是否为null值或者空元素集合。
这里附上org.apache.commons.lang3
类的pom引用片段,方便大家参考
|
|
处理字符串时一些避免NPE的技巧
在进行字符串String相关操作时候,有两个简单的技巧可能避免潜在的空指针问题。
一是,在字符串比较时,如果是变量和一个常量比较,使用常量的equals方法,例如:
|
|
如果是两个String变量比较,而且这两个变量都有可能为null值,不论使用哪个变量的equals()
方法都有可能造成潜在的NPE错误。这种情况下,我们可以使用Objects.equals(x, y)
方法进行值相等的判断。Objects.equals
方法有一个地方需要特别注意,如果进行比较的两个变量都为null,会返回true,该返回的true值是否符合逻辑,需要在业务场景下判断。
二是,将其它类型变量转换成String时,用 String.valueOf(foo)
而非 foo.toString()
。
|
|
自己定义方法时 null 的返回
NPE出现的根本原因是开发者忽略了潜在的空值,那么我们在写自己方法时候,就尽量避免返回null值,从源头上解决NPE问题的出现。
用其它表示空的值替代null
首先,我们可以考虑一下自己所写的方法是否真的有必要返回null值,方法返回null值的意义是什么,是否可以用其它表示“空”含义的值替换null。例如,如果方法声明返回的是一个List,我们可以考虑返回空List Collections.emptyList()
而非null;同样的,如果方法要返回的是String,我们也可以考虑使用空字符""
来替代null。
null无法被替换的情况
当然,并非一味替换null就是合适的,我们需要结合具体业务场景,例如,当你去解析外部服务的请求结果,并返回一个List时,可能你需要用null去表达服务调用失败,来避免混淆实际返回结果为空List的情况。
在这些业务场景下,null值有着无法被替代的意义,返回null可能是最好的解决方案,这时也有一些方法可以避免潜在的NPE错误。
- 返回 Optional
显式地声明可能存在的空值。将方法要返回null的地方改为返回 Optional.empty()。当自己或其他开发者在调用该方法时,会被强制要求考虑返回值可能为空的情况,从而避免NPE的出现。具体关于Optional的介绍,可以参考https://www.baeldung.com/java-optional。 - 在可能返回空值的方法上方加入
@Nullable
修饰,这样IDEA会在该方法调用处提醒处理可能的空值。
|
|
- 使用空对象设计模式,这个方法可能不太常用,这里不作赘述,具体可以参考 https://www.runoob.com/design-pattern/null-object-pattern.html。
小结
- 接收外部数据时候,需意识到潜在null值的出现。方法名有get时候,考虑到返回值可能为null
- 字符串操作时,
foo.equals("some string")
替换为"some string".equals(foo)
,foo.toString()
替换为String.valueOf(foo)
- 可以利用
Optional.ofNullable(...).map(...).map(...).orElse()
的写法处理多层级嵌套时,get中出现null的情况 - 开发新方法时,如果返回List,可以考虑返回空列表
Collections.emptyList()
来替代返回null - 开发新方法时,如果返回String,可以考虑返回空字符串来替代返回null
- 开发新方法时,如果确实需要返回null,可以考虑使用Optional显式声明空值
StringUtils.isEmpty()
可以同时判断字符串为null或空字符串“”的情况,StringUtils.isBlank()
还可以判断字符串只包含空格的情况CollectionUtils.isEmpty()
可以判断一个集合为null或空元素集合的情况
参考资料
- https://stackoverflow.com/questions/3458451/check-chains-of-get-calls-for-null
- https://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it
- https://stackoverflow.com/questions/271526/avoiding-nullpointerexception-in-java
- https://www.baeldung.com/java-optional
- https://www.runoob.com/design-pattern/null-object-pattern.html