目录

前言

今天不是介绍 name.equals(“ali”) 改成 “ali”.equals(name)等技巧,我个人并不喜欢这些技巧,因为它只解决了当前这句代码不会抛出空指针,后续用到name的地方还是可能抛出空指针,没有从根本上解决问题(其实我更建议使用第一种,如果有空指针,快速失败,而不是模棱两可的继续执行下去)

今天不讨论局部避免空指针的技巧,而是讨论如何使用一些方法和原则从根源上解决空指针问题

空指针出现的情况

snippet.java
    public void test(Integer id, List<Integer> 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
    }
  1. 函数参数为null
  2. 通过函数返回值得到null
  3. 集合中有null元素
  4. 对象中的字段可能为null

问题1:函数参数为null?怎么解决?

snippet.java
解决方案:
约定函数的参数一定不能为null,否则函数的行为不可预测,由调用者去保证参数的合法性

问答:为什么不可以在函数内判断参数是否为空

  1. 可以判断,但如果判断为空后该怎么做?答案是什么都做不了,因为调用方才最清楚没有数据的时候该怎么做
    1. 函数定义时只关注如何根据数据计算出结果,不考虑没有数据的情况
  2. 简化函数定义时的复杂度,增加可阅读性
  3. 降低测试用例成本,不用考虑太多的corner case
  4. 注意:如果该函数是对外暴露的接口函数,则需要校验每个参数

关于集合为空(list.size()==0)的问题,我觉得也应该放在调用方判断,因为从业务上来说跟null没什么区别。

问题2:通过函数返回值得到null?怎么解决?

snippet.java
解决方案:
  - 如果是不信任的函数(应用间接口,某些二方库函数),将返回值用Optional包装
  - 应用内部函数返回一定不能为null,如果可能为null,则用Optiona包装类型

问答:为什么不可以返回null,让调用方判断?

  1. 我们要避免null产生,也要避免null蔓延 (不能蔓延到外部)
  2. 降低调用方判空成本,否则调用方可能将每个函数的返回值都用Optional包装起来,程序也会变得丑陋。

问答:哪些函数可能返回null,哪些函数不会返回null

问题3:集合中有null元素?怎么解决?

snippet.java
解决方案:
推荐使用禁止存放null的集合,将判断空元素的过程前移到数据生成时

没找到合适的类库(大家有知道的可以推荐下哈),所以之前自己写了一个(仅供参考) https://github.com/fangqiang/safe-container

问题4:对象中的字段可能为null?怎么解决?

snippet.java
解决方案:
1. 规范字段定义。对象中每个字段只能有2种状态
  1. 字段如果在整个生命周期中可能为null,那定义时就用Optional包装类型  
  2. 非Optional包装字段,表明该字段在实例化后就一直不为null

总结

核心原则就是让每个变量从创建的那一刻起就不是null,这样无论函数之间怎样调用来调用去都不会出现null。如何做到每个变量创建时就不是null?

对开发人员的要求

  1. 通过上述3点原则保证整个程序中没有null变量生成
    1. 某些场景可能会产生null时(为了性能不用Optional),也要防止null蔓延到其他函数
  2. 编写函数时明确函数的参数、返回值 (这就是一个函数的契约)
    1. 契约最好利用语言特性(如Optional)实现。强制调用方遵守
    2. 如果语言特性不能支持,可以使用注解、文档暴露给调用方。提示调用方遵守
  3. 调用函数时要遵守契约(契约精神)
snippet.java
    /**
     * 最后给个例子说明契约式编程的的简洁
     */
 
    /**
     * 防御式编程
     */
    public Integer get(Integer percent, List<Integer> 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<Integer> counts){
 
        // 业务相关的校验还是由本函数完成
        if(percent < 0){
            throw new RuntimeException();
        }
 
        int sum=0;
        for (Integer count : counts) {
            sum += count;
        }
        return sum;
    }