当前位置: 首页 > news >正文

Java null最佳实践

好的习惯

什么时候要考虑判空呢?最常见的就那么三种情况

  • 使用调用某个方法得到的返回值之前,方法的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工具,来帮助程序员快速的做参数检测,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的ObjectUtils,Apache Common Lang的 ObjectUtils (这个工具类其实非常强大,里边函数的处理Null的思路也非常值得借鉴)等,举一个例子。假设一个场景,需要多对象的判空逻辑,就可以使用工具类的线程函数,让语义更清晰,减少错误。

//比较冗长
if(obj1 == null || obj2 == null || boj3 == null){
    //do something
}


//ObjectUtils 的方式:语义直接,不易出错
(if(ObjectUtils.anyNotNull(obj1,obj2,obj3))){
    // do something
}

复制代码

也有框架提供了类似于Assert的 工具类,如Lombok 的 @NonNull注解,如果被注解的对象是空值,直接会抛出NPE,用作对输入参数的检查,会让代码变优雅不少,**语义也更清晰**。类似这样的工具遵循了JSR305, 具体实现有很多,比如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年代就提出来了,其核心观念是:“预防你认为不可能发生的,时间长了,它一定会发生。”,防御性编程里有好多套路:永远不要相信用户输入,调用时永远做异常判断等等等等。有一些防御性编程的意识是一件非常好的事儿,能防止很多低级错误产生。但是一些不必要的严防死守,会让代码变得很丑陋和复杂。很多事儿,过犹不及。

码字不易,如有建议请扫码


相关文章:

  • 36氪首发|「优仕美地医疗」获亿元级B轮融资,要打造日间手术机构的连锁服务网络...
  • 阿里云联合8家芯片商推“全平台通信模组”,加速物联网生态建设
  • MySQL设置主从复制
  • 赶紧收藏!新鲜出炉的重庆轨道交通图 首末班时间和线路都在里面
  • 厉害!重庆参加马拉松赛人数7年翻10倍,今年区县马拉松赛事将大增
  • python教程(一)·命令行基本操作
  • TCP三次握手四次挥手
  • C++类中的特殊成员函数
  • ES搜索引擎集群模式搭建【Kibana可视化】
  • spring cloud gateway 源码解析(4)跨域问题处理
  • 有赞电商云应用框架设计
  • JS专题之继承
  • 阿里云服务器怎么升级配置?升级有哪些限制?
  • UniDAC使用教程(五):数据加密
  • React-生命周期杂记
  • 深入了解以太坊
  • 【Leetcode】101. 对称二叉树
  • Android系统模拟器绘制实现概述
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • Bootstrap JS插件Alert源码分析
  • CSS 三角实现
  • ES6系列(二)变量的解构赋值
  • golang中接口赋值与方法集
  • 编写符合Python风格的对象
  • 第三十一到第三十三天:我是精明的小卖家(一)
  • 和 || 运算
  • 机器学习 vs. 深度学习
  • 基于Dubbo+ZooKeeper的分布式服务的实现
  • 基于Javascript, Springboot的管理系统报表查询页面代码设计
  • 基于游标的分页接口实现
  • 解决iview多表头动态更改列元素发生的错误
  • 利用jquery编写加法运算验证码
  • 如何打造100亿SDK累计覆盖量的大数据系统
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • 微服务框架lagom
  • 学习Vue.js的五个小例子
  • zabbix3.2监控linux磁盘IO
  • ​如何在iOS手机上查看应用日志
  • #etcd#安装时出错
  • #我与Java虚拟机的故事#连载12:一本书带我深入Java领域
  • ${ }的特别功能
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (01)ORB-SLAM2源码无死角解析-(66) BA优化(g2o)→闭环线程:Optimizer::GlobalBundleAdjustemnt→全局优化
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (附源码)ssm经济信息门户网站 毕业设计 141634
  • (附源码)计算机毕业设计SSM智慧停车系统
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (十六)一篇文章学会Java的常用API
  • (转)VC++中ondraw在什么时候调用的
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**
  • .[hudsonL@cock.li].mkp勒索病毒数据怎么处理|数据解密恢复
  • .gitignore文件—git忽略文件
  • .Net - 类的介绍
  • .Net 8.0 新的变化
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑