===== 前言 ===== 今天不是介绍 name.equals("ali") 改成 "ali".equals(name)等技巧,我个人并不喜欢这些技巧,因为它只解决了当前这句代码不会抛出空指针,后续用到name的地方还是可能抛出空指针,没有从根本上解决问题(其实我更建议使用第一种,如果有空指针,快速失败,而不是模棱两可的继续执行下去) 今天不讨论局部避免空指针的技巧,而是讨论如何使用一些方法和原则从根源上解决空指针问题 === 空指针出现的情况 === ```java public void test(Integer id, List counts){ for (Integer count : counts) { // 参数counts 可能为null System.out.println(count.toString()); // 集合中的元素可能为null } String sid = id.toString(); // 参数id可能为null Customer customer = getCustomerByID(sid); // 函数返回值肯能为null System.out.println(customer.getName().length()); // 对象中的属性可能为null } ``` - 函数参数为null - 通过函数返回值得到null - 集合中有null元素 - 对象中的字段可能为null ===== 问题1:函数参数为null?怎么解决? ===== ```java 解决方案: 约定函数的参数一定不能为null,否则函数的行为不可预测,由调用者去保证参数的合法性 ``` === 问答:为什么不可以在函数内判断参数是否为空 === - 可以判断,但如果判断为空后该怎么做?答案是什么都做不了,因为调用方才最清楚没有数据的时候该怎么做 - 函数定义时只关注如何根据数据计算出结果,不考虑没有数据的情况 - 简化函数定义时的复杂度,增加可阅读性 - 降低测试用例成本,不用考虑太多的corner case - 注意:如果该函数是对外暴露的接口函数,则需要校验每个参数 关于集合为空(list.size()==0)的问题,我觉得也应该放在调用方判断,因为从业务上来说跟null没什么区别。 ===== 问题2:通过函数返回值得到null?怎么解决?===== ```java 解决方案: - 如果是不信任的函数(应用间接口,某些二方库函数),将返回值用Optional包装 - 应用内部函数返回一定不能为null,如果可能为null,则用Optiona包装类型 ``` === 问答:为什么不可以返回null,让调用方判断? === - 我们要避免null产生,也要避免null蔓延 (不能蔓延到外部) - 降低调用方判空成本,否则调用方可能将每个函数的返回值都用Optional包装起来,程序也会变得丑陋。 === 问答:哪些函数可能返回null,哪些函数不会返回null === * 字典映射类的函数(可能返回null) * 从map中取值 * 从存储中取值 * 算法类函数(一般不可能返回null) * String 小写转大写 ===== 问题3:集合中有null元素?怎么解决? ===== ```java 解决方案: 推荐使用禁止存放null的集合,将判断空元素的过程前移到数据生成时 ``` 没找到合适的类库(大家有知道的可以推荐下哈),所以之前自己写了一个(仅供参考) https://github.com/fangqiang/safe-container ===== 问题4:对象中的字段可能为null?怎么解决? ===== ```java 解决方案: 1. 规范字段定义。对象中每个字段只能有2种状态 1. 字段如果在整个生命周期中可能为null,那定义时就用Optional包装类型 2. 非Optional包装字段,表明该字段在实例化后就一直不为null ``` ===== 总结 ===== 核心原则就是让每个变量从创建的那一刻起就不是null,这样无论函数之间怎样调用来调用去都不会出现null。如何做到每个变量创建时就不是null? - 规范对象属性(要么一定不为空,要么一定是Optional类型) - 调用可能返回null的函数,返回值用Optional包装起来。 - 使用禁止存放null的容器 ###对开发人员的要求 - 通过上述3点原则保证整个程序中没有null变量生成 - 某些场景可能会产生null时(为了性能不用Optional),也要防止null蔓延到其他函数 - 编写函数时明确函数的参数、返回值 (这就是一个函数的契约) - 契约最好利用语言特性(如Optional)实现。强制调用方遵守 - 如果语言特性不能支持,可以使用注解、文档暴露给调用方。提示调用方遵守 - 调用函数时要遵守契约(契约精神) ```java /** * 最后给个例子说明契约式编程的的简洁 */ /** * 防御式编程 */ public Integer get(Integer percent, List counts){ if(percent == null || counts == null){ return null; } if(counts.size() == 0){ return null; } if(percent < 0){ throw new RuntimeException(); } int sum=0; for (Integer count : counts) { if(count != null){ sum += count; } } return sum; } /** * 契约式编程 * * @param percent percent 必须大于0,否则抛出异常 * @param counts @NotEmpty注解提示调用方先判断size() > 0 * NoneNulList保证了容器中的元素不可能为null,将这判空这个过程前移到数据产生时 * @return */ public Integer get2(Integer percent, @NotEmpty NoneNulList counts){ // 业务相关的校验还是由本函数完成 if(percent < 0){ throw new RuntimeException(); } int sum=0; for (Integer count : counts) { sum += count; } return sum; } ```