好的习惯
什么时候要考虑判空呢? 最常见的就那么三种情况
使用调用某个方法得到的返回值之前, 方法的 API 说明中明确指出可能会返回空, 或者 API 文档不靠谱.
使用传入的参数前.
获取到一个多层嵌套对象, 使用内层对象之前(链式调用尤其要小心).
如果不做良好的判空处理, NullPointerException 就会发生, 有的时候会引发很致命的故障.
除了上面三种情况, 再根据我的经验列举一些发生 NPE 的常见情况:
OR 映射的时候, 一些预期中不会为空的数据变成了空, 框架又没有做防御性处理.
常见的低级错误: 调用 toString(), compare()等方法不判空 (统计出来的 N 多故障是 toString() 的时候出现的空, 很低级, 但是事实如此).
异步调用, 结果值没有返回时就使用.(看一下 Future)
调用超时处理不当.
RPC 的被调用方做了修改, 没通知调用方 / RPC 调用失败(很多原因, 如鉴权废了, 底层链接有问题, 超时等).
基本类型的包裹型实例如果被赋值为空, 且被自动拆箱时.
syncronized 了一个空对象.
那么如何较好的处理 null 呢?
使用前判空. 这是最常见的使用办法.
- if (obj != null){
- //do something
- }
较好的编程方式是提前判断错误(可以参考 Guard Clause 模式), 这样能够消除过度嵌套的情况出现.
- if(obj == null){
- // 错误处理, 一般是返回约定的错误, 或者抛 Exception
- }
也 ** 可以使用一些工具类减少代码量, 让编程模式更清晰 **. 例如 google 的 Guava 框架提供了 Preconditions 工具 https://www.baeldung.com/guava-preconditions , 来帮助程序员快速的做参数检测, Preconditions 里有一个静态方法 checkNotNull, 如果不为空, 则返回被检测的对象本身, 如果被检测的对象为空, 则会抛出 NullPointerException.
- @Test
- public void testGuavaNotNull(){
- Object obj = null;
- String errorMessage = "obj is null";
- Preconditions.checkNotNull(obj,errorMessage);
- }
- // 一般的使用方式是这样的 对于一个输入参数或者调用其它方法返回的值 objToBeChecked
- try{
- ...
- // 异常消息收集或构造
- obj = Preconditions.checkNotNull(objToBeChecked,errorMessage);
- ...
- }catch(NullPointerException npe){
- // 异常处理
- }
Java 基础库的框架中其实也提供了简单的静态方法 java.util.Objects.reqireNonNull(T obj), 但是 Guava 框架的好处是, 你可以构造具体的 errorMessage 传递给检测函数, 从而在异常被抛出后, 程序员可以得到更具体的异常信息.
空处理常见的工具类还有 Spring 的, Apache Common Lang 的 (这个工具类其实非常强大, 里边函数的处理 Null 的思路也非常值得借鉴)等, 举一个例子. 假设一个场景, 需要多对象的判空逻辑, 就可以使用工具类的线程函数, 让语义更清晰, 减少错误.
- // 比较冗长
- if(obj1 == null || obj2 == null || boj3 == null){
- //do something
- }
- //ObjectUtils 的方式: 语义直接, 不易出错
- (if(ObjectUtils.anyNotNull(obj1,obj2,obj3))){
- // do something
- }
也有框架提供了类似于 Assert 的 工具类, 如 Lombok 的 @NonNull https://projectlombok.org/features/NonNull 注解, 如果被注解的对象是空值, 直接会抛出 NPE, 用作对输入参数的检查, 会让代码变优雅不少,** 语义也更清晰 **. 类似这样的工具遵循了 JSR305 https://jcp.org/en/jsr/detail?id=305 , 具体实现有很多, 比如 findbugs,SpotBugs,Spring,AndroidTookit 等都提供了这样的注解. 注解可以非常方便的挂在方法, 输入参数上.
- //Lombok 的例子, 如果 obj 为 null, 直接抛出 NPE 异常,
- public void LombokNullCheck(@NonNull Object obj){
- // 可以直接使用 obj
- }
Java8 提供的改进, Java8 提供了一个叫做 Optional 的类型, 在实战中非常实用, Optional 类型和 stream API 一并使用的话, 能让空检查变得更加优雅, 特别是复杂嵌套对象的空检查. 但读很多人的代码, 发现他们并没有习惯这样使用. 先上一段代码感受一下.
- class Passenger{
- private Seat seat;
- private Cert cert;
- Cert getCert(){
- return cert;
- }
- ...
- }
- class Cert{
- private PersonalInfo pi;
- PersonalInfo getPersonalInfo(){
- return pi;
- }
- }
- class PersonalInfo{
- private String name;
- String getName();{
- return name;
- }
- }
对于嵌套比较深的类, 下面这样的代码太常见了, 大段的 && 条件判断非常容易出错,** 代码可读性也非常差.**
- Passenger Passenger = SomeMehtod.getPassenger();
- if(Passenger != null && Passenger.getCert() != null
- && Passenger.getCert().getPersonalInfo != null){
- return Passenger.getCert().getPersonalInfo().getName();
- }
- else return "default name";
- // 有更差的实践是写成下面的多重嵌套 if 模式, 这样在真实情况下很容易缩进七八层, 甚至十几层, 代码可读性基本上就没了.
- if(Passenger != null){
- if (Passenger.getCert() != null){
- if(Passenger.getCert().getPersonalInfo() != null){
- return Passenger.getCert().getPersonalInfo().getName();
- }else return "default name"
- }
- }
- // 还有更差的实践, 比如生成很多只用一次的中间对象. 对了, 就是把 Cert,PersonalInfo 再都 new 出来. 代码太难看, 就不补全了.
使用 Optional 配合 lambda 表达式的效果, 见下面代码, 是不是非常简洁清晰了?
- Passenger Passenger = SomeMehtod.getPassenger();
- return Optional.ofNullable(Passenger)
- .map(Passenger::getCert)
- .map(Cert::getPersonalInfo)
- .map(PersonalInfo::getName)
- .orElse("default name");
如果, 需要抛出一个空指针异常而不是返回默认值, 可以写成下面这样.
- Passenger Passenger = SomeMehtod.getPassenger();
- return Optional.ofNullable(Passenger)
- .map(Passenger::getCert)
- .map(Cert::getPersonalInfo)
- .map(PersonalInfo::getName)
- .orElesThrow(NullPointerException::new)
对于嵌套类的空判断, 使用 Optional 比传统的层层剥皮判断要好很多. 其他新一些的语言如 Swift,Kotlin 都提供了内建语言支持, 会更加优雅, 代码量也会少很多. 需要特别说明的是, 最好通读 Java8 的 Stream API 文档, 了解兰布达表达式的正确使用方式, 才能以正确的方式打开. 如果不结合 Stream API 来看, Optional 反而让代码变得更冗余了.
作为调用方的义务
尽量不要把 null 当做一个参数传递. 这个其实很好了解, 当你传入一个 null 的时候, 如果不知道被调方法的具体实现, 你不知道会触发什么. 假如被调用函数没做空处理, 假如这个 null 又被传递了出去, 影响就不可控了, 除非你知道被调用方法的所有具体实现.
作为 API 提供方的义务
对传入的参数做判空处理; 良好的 API 文档标明传入空值的后果和什么情况下会抛出 NPE 或者包装了的其它异常. 使用 @NonNull 这种 assert 工具; 不要继续传递传入的空值. 抛出 NPE 比返回一个 null 要好的多(如果考虑性能影响则另当别论), 尽量不返回 null, 如果必须要, 文档一定要说明.
单元测试的防护网很可能救你一命. 一些代码的生命周期很长. 有些地方的判空处理如果有对应的单元测试覆盖这部分逻辑, 当其他维护者 (非常有可能是其他维护者) 不小心修改了这部分逻辑, 对应的单元测试很可能会救系统一命. 它会提醒新来的维护者: 你踩了个地雷, 好好看看是不是应该这样. 我见过好几次这样的救命案例了.
NPE 一定要严防死守么? 答案是否定的. 识别异常的含义, 并正确利用, 是一个程序员的素养.
一个复杂嵌套对象中, 我们要对所有的字段判空么? 那岂不是要写死人了. 文章最初的那个例子就是这样: 如果对所有字段全写判空, 代码量会很感人, 读起来会更感人. 其实这个问题也是有解的, 如果你只关注部分字段, 就只对它们判空并读取, 别的字段别碰. 如果必须要碰, 在外层做 catch(会影响性能, 别在性能关键点做). 另外, 读取的数据源其实应该有很好的注释, 并应该有 nullable 的 assert, 修改数据的人应该仔细读这些注释, 并不去破坏规则, 毕竟软件是多人协作. 大家都遵守约定, 才能降低协作中产生的错误概率.
再聊两句防御性编程: 防御性编程上世纪 80 年代就提出来了, 其核心观念是:"预防你认为不可能发生的, 时间长了, 它一定会发生.", 防御性编程里有好多套路: 永远不要相信用户输入, 调用时永远做异常判断等等等等. 有一些防御性编程的意识是一件非常好的事儿, 能防止很多低级错误产生. 但是一些不必要的严防死守, 会让代码变得很丑陋和复杂. 很多事儿, 过犹不及.
来源: https://juejin.im/post/5c4fb52ef265da615f77a1f3