开发小技巧系列 - 如何避免NullPointerException?(二)

1,133 阅读9分钟

本文已参与「新人创作礼」活动,一起开 启掘金创作之路。

开发小技巧系列文章,是本人对过往平台系统的设计开发及踩坑的记录与总结,给初入平台系统开发的开发人员提供参考与帮助。

先来回答上篇文章中“思考”的2个问题。1、 这段程序为什么不用“==”号了? “==”号在数字较大时会有什么问题?

答:在对象类型中,使用“==”时,一般是比较的地址,而不是具体的值,当然Integer(int)有点除外,它会将-128~127的值缓存起来,在这个范围内,使用“==”进行比较,是没有问题,但是超过这个范围,使用“==”号,就会返回不是预期的效果。因此,在程序中,尽量使用equals,来避免埋下的坑。(建议使用Objects.equals),这样可以防止xx.equals(...)表达式中的 xx 为NPE的情况。

2、 @NotNull 有什么作用?

答:这个@NotNull起到修饰说明的作用,提醒使用者,注意传入的参数值,对程序运行不起作用。悬浮时会有如下的提示:

640.png

案例二:

在开发的过程中,不可避免地需要从对象中获取属性的值,比如order.orderNo(订单对象.订单号),那这时相信很多小伙伴就在头疼了,order对象到底会不会为null呢? 想到头昏脑涨,最后还是把 null != order加上,比如下面的代码:

MemberService.java

    /**
     * 传统的定法,就是先判断对象是否为null,不为null,
     * 则进行转换操作,否则,返回null
     * @param member
     *      输入的会员对象
     * @return
     */
    public MemberDTO transfer(Member member){
        if(null != member){
            MemberDTO memberDTO = new MemberDTO()
                    .setMemberId(member.getMemberId()) 
                   .setGender(member.getGender())
                    .setGenderName(ProgrammerA.getGender2(member.getGender()))
                    .setNickName(member.getNickName())
                    .setRealName(member.getRealName());
            return memberDTO;
        }
        return null;
    }

这只是一个小例子,当程序中需要对多种业务对象进行操作时,肯定会有一堆这样的判断。那么有什么更好的解决办法呢?

在JDK8 中有一个新的特性:Optional, 是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。使用它,可以让代码更加简单,可读性跟高,代码写起来更高效。

结合JDK8的 function,可以设计一个对象互转的模板方式,来应从对象A转到对象B,模板方法通过接收一个function来实现个性化的方法,由具体的调用方来提供,比如下面的例子,有会员,部门的对象的转换。

OptionalUtils.java

    /**
     * 属性互转模板方法
     * @param source
     *  原对象(需要比较的对象)
     * @param function
     *  对象E->R的赋值过程
     * @param defaultObject
     *  默认值
     * @param <E>
     *      原始对象
     * @param <R>
     *      结果对象
     * @return
     */
    public static <E, R> R transfer(E source, Function<E, R> function, R defaultObject){
        Optional<E> optional = Optional.ofNullable(source);
        if(Optional.ofNullable(source).isPresent()){
            return function.apply(optional.get());
        }
        return defaultObject;
    }

然后编写如下代码:

MemberService.java

    /**
     * 通过Optional的方式来处理转换,减少编写 xx != null 这样的表达式
     * @param member
     *      输入的会员信息
     * @param defaultDto
     *      默认的值(如果为null时)
     * @return
     */
    public MemberDTO transferByOptional(Member member, MemberDTO defaultDto){
        return OptionalUtils.transfer(member, MemberService::convert, defaultDto);
    }    /**
     * 将member的属性 赋值给memberDTO
     * @param member
     *      输入的会员信息
     * @return
     */
    public static MemberDTO convert(Member member){
        MemberDTO memberDTO = new MemberDTO()
                 .setMemberId(member.getMemberId()) 
                 .setGender(member.getGender())
                 .setGenderName(ProgrammerA.getGender2(member.getGender()))
                .setNickName(member.getNickName())
                .setRealName(member.getRealName());
        return memberDTO;
    }
    /**
     * 将部门对象转换成DTO
     * @param dept
     * @return
     */
    public static DeptDTO cover(Dept dept){
        DeptDTO deptDTO = new DeptDTO();
        deptDTO.setDeptId(dept.getDeptId()); 
        deptDTO.setParentId(dept.getParentId());
        deptDTO.setDeptName(dept.getDeptName());
        return deptDTO;
    }

编写测试用例:

OptionalTest.java

    /**
     * 正常情况下的调用测试(非null)
     */
    @Test
    public void optionalTest(){
        Member member = new Member();
        member.setId(1);
        member.setMemberId(1000);
        member.setNickName("测试");
        member.setGender(1);
        MemberDTO memberDTO = memberService.transferByOptional(member, null);
        log.debug("MemberDTO: {}", memberDTO);
        //声明一个部门
        Dept dept = new Dept();
        dept.setId(1);
        dept.setDeptId(100);
        dept.setParentId(0);
        dept.setDeptName("部门");        DeptDTO deptDTO = OptionalUtils.transfer(dept, MemberService::cover, null);
    }
    //程序输出结果:
    // [main] DEBUG net.jhelp.demo.OptionalTest - MemberDTO(memberId=1000, nickName=测试, realName=null, gender=1, genderName=男)
    // [main] DEBUG net.jhelp.demo.OptionalTest - deptDTO : DeptDTO(deptId=100, parentId=0, deptName=部门)
      /**
       *  测试传入对象是null的情况
      */
    @Test
    public void optionalWithNullTest(){
        MemberDTO memberDTO = memberService.transferByOptional(null, null);
        log.debug("optionalWithNullTest:{}", memberDTO);
    }
    //输出结果:
    //DEBUG net.jhelp.demo.OptionalTest - optionalWithNullTest:null

使用Optional的特性,可以减少编写众多null != obj来防止NPE的问题,通过“模板方法”的方式,可以减少更多的重复代码的编写。上面的“模板方法”的类,小伙伴可以拿到项目中直接使用,只需要编写一个赋值的方法就可以。

案例三:

上面的场景是用于对象和对象之间互转,但有时候在开发的过程中,只需要获取对象中的某个属性的值,可能程序中的不同业务/方法,需要用到不同属性的值。可能都需要编写类似如下的代码:

//以上面订单为例//想获取订单的orderNo(订单号)
if(null != order){
  return order.getOrderNo();
}//另外的方法想获取金额
if(null != order){
  return order.getAmount();
}//获取订单上的买家名称
if(null != order){
  return order.getBuyerName();
}//可能还会有其他的需求,获取不同的值

这种现象相信在程序中是无处不在的,有什么办法来简化这个过程,减少if(null != obj)这样的代码的编写呢,答案是有的,可以使用Optional来编写一个模板方法。

OptionalUtils.java

    /**
     * 获取对象的属性(带判断null)
     * @param source
     *  原对象(需要比较的对象)
     * @param supplier
     *  工厂方法
     * @param defaultObject
     *  默认值
     * @param <E>
     *     输入的对象
     * @param <R>
     *     输出的值
     * @return
     */
    public static <E, R> R getAttr(E source, Supplier<R> supplier, R defaultObject) {
        if(Optional.ofNullable(source).isPresent()) {
            return supplier.get();
        }else{
            return defaultObject;
        }
    }

这里用到了JDK1.8中提供的Supplier,这个相当于是一个工厂的方法,与function不同的是它不接受参数,直接为我们生产指定的结果,有点像生产者模式。Supplier 接口可以理解为一个容器,用于装数据的,Supplier 接口有一个 get 方法,可以返回值。

来编写一个测试用例,看下上面模板方法的效果。

OptionalTest.java

     /**
     * 测试从对象中获取某个值
     */
    @Test
    public void propertiesGetTest(){
        Member member = new Member();
        member.setId(1);
        member.setMemberId(1000);
        member.setNickName("测试");
        member.setGender(1);
        member.setRealName("java");
        String attr = OptionalUtils.getAttr(member, ()-> member.getNickName(), null);
        log.debug("nickName: {} ", attr);        String realName = OptionalUtils.getAttr(member, ()-> member.getRealName(), null);
        log.debug("realName: {} ", realName);        Integer memberId = OptionalUtils.getAttr(member, ()-> member.getMemberId(), null);
        log.debug("memberId: {} ", memberId);
    }

执行的结果

11:12:14.156 [main] DEBUG net.jhelp.demo.OptionalTest - nickName: 测试 11:12:14.171 [main] DEBUG net.jhelp.demo.OptionalTest - realName: java 11:12:14.172 [main] DEBUG net.jhelp.demo.OptionalTest - memberId: 1000

从测试的结果上看,完全能满足预期的效果,来测试下,如果对象是Null时情况,代码如下:

    /**
     * 测试从对象中获取某个值(对象是NULL)
     */
    @Test
    public void propertiesGetWithNullTest(){
        Member member = null;        String attr = OptionalUtils.getAttr(member, ()-> member.getNickName(), null);
        log.debug("nickName: {} ", attr);        String realName = OptionalUtils.getAttr(member, ()-> member.getRealName(), null);
        log.debug("realName: {} ", realName);        Integer memberId = OptionalUtils.getAttr(member, ()-> member.getMemberId(), null);
        log.debug("memberId: {} ", memberId);
    }

运行结果(没有见到异常信息)

11:15:18.027 [main] DEBUG net.jhelp.demo.OptionalTest - nickName: null 11:15:18.041 [main] DEBUG net.jhelp.demo.OptionalTest - realName: null 11:15:18.042 [main] DEBUG net.jhelp.demo.OptionalTest - memberId: null 

上面的案例二,案例三的处理方法,是可以很好的防止及减少重复代码的编写,但是如果对象就是NULL,那返回的结果还是为NULL,下游的开发人员来是要来处理这个NULL。其实,也可以通过引入一个"默认对象(值)"的概念。可以采用以下规则:

  1. 对象是NULL的,可以返回一个“空对象(EmtpyObject)”;

  2. 值为空的,可以返回默认值,比如空字符串(""), 数字类型的(0);

空对象

可以在DTO对象上,定义一个默认空对象,然后在程序中,返回这个空对象,那下游的开发人员,就不用去担心NULL,不用在加上 null != obj 这样的判断。还是以上面的“会员DTO”为例,来看下怎么写:

MemberDTO.java

   /**
     * 定义一个空的对象
     */
    public static final MemberDTO EMPTY_MEMBER = new MemberDTO();

这样,就拥有一个“空对象”,在后台数据没有对应的“会员”时,就可以返回这个“空对象”,下游的开发人员就不会因为调用

member.getNickName(); //

而报空指(NPE)的错误。

但是这样又会有一个新的问题,下游的开发人员,怎么知道方法返回的对象是“正常对象”,还是“空对象”呢?当然,从业务的角度来说,对象肯定有它的唯一属性,可以通过判断它的值,来确认是不是“空对象”。比如会员的唯一属性是“会员ID”,可以通过它来判断是否是“空对象”。

if(Objects.isNull(member.getMemberId())){
  //这是一个空对象  //看上去,是不是又回到原来的判断NULL去b
}

但是从这个代码上,感觉又陷入的死胡同里,方法返回了空对象,下游开发人员又在又要去针对对象的特定属性进行判断(null != obj)。那么有没有更好的办法呢?来看下面的代码

EmptyDTO.java

@Data
public abstract class EmptyDTO {
    /**
    *  标志对象是否为空,默认是“非空”对象。
     */
    private Boolean empty = false;    public EmptyDTO(){}    public EmptyDTO(Boolean empty){
        this.empty = empty;
    }
}

然后对原来MemberDTO类进行调整,继续EmptyDTO,添加一个构造方法,变成MemberDTO2.Java

    /**
     * 带参数的构造函数(是否空)
     * @param empty
     */
    public MemberDTO2(Boolean empty){
        setEmpty(empty);
    }

    /**
     * 定义一个空的对象(通过 isEmpty来判断是否为空)
     */
    public static final MemberDTO2 EMPTY_MEMBER = new MemberDTO2(true);

从上面的代码看,添加了一个属性,来标准当前的对象,是否是“空对象”,减少下游开发人员的烦脑,只要需调用一下isEmpty()方法就可以了,是不是问题就变得简单多了。

    /**
     *  测试传入对象是null的情况
     */
    @Test
    public void optionalWithNullTest2(){
        MemberDTO2 memberDTO = memberService.transferByOptional2(null, MemberDTO2.EMPTY_MEMBER);
        log.debug("optionalWithNullTest2:{}", memberDTO);
        log.debug("是否是空对象:{}", memberDTO.isEmpty());
    }

11:00:05.242 [main] DEBUG net.jhelp.demo.OptionalTest - optionalWithNullTest2:MemberDTO2(memberId=null, nickName=null, realName=null, gender=null, genderName=null)11:00:05.247 [main] DEBUG net.jhelp.demo.OptionalTest - 是否是空对象:true

总结一下

本篇主要是对对象是否为NULL(NULL!=obj)这情况,及对象转换成另外的对象的过程进行总结,通过JDK1.8 提供的新特性,来解决程序中大量的判断语句,让程序更简洁清晰;另外一个引入一个“空对象”的概念,来更好的解决程序与程序调过程中的引藏的NULL。

1. 对象向对角之间赋值的NULL检查;

2. 从对象中获取属性时NULL的检查;

3. 运用Optional, function 的新特殊,模板方法。

4. 空对象的引入,减少NPE的出现。

如果想要上面的代码,可以访问此仓库。

gitee.com/TianXiaoSe_…

题外话,可能在开发过程中,经常需要去判断属性是否有值,没值要给个默认值,比如如下的代码,基本上是三元表过式(这是比较的方法,而大部分初入开发,可能都是if(...){}else{}这样的结构),这种有没有更好的解决方式呢?

//假设有一个销售数据的对象(里面有订单金额,订单量,交易金额,成交商品数,成交客户数,客单值等
//需木给前端返回值对象(如是NULL,则返回0)
//可能的代码会是如下:
//销售数据对象
SellDataInfo sellDataInfo = ...;
//返回给前端的
DTORevenueIndicatorDTO dto = new RevenueIndicatorDTO();
dto.setOrderCount(sellDataInfo.getOrderCount() != null ? sellDataInfo.getOrderCount() : 0);
dto.setGmvAmount(sellDataInfo.getGmvAmount() != null ? sellDataInfo.getGmvAmount() : 0);
dto.setBuyerCount(sellDataInfo.getBuyerCount() != null ? sellDataInfo.getBuyerCount() : 0);....

开发小技巧系列文章:

1. 开发小技巧系列 - 库存超卖,库存扣成负数?

2. 开发小技巧系列 - 重复生成订单

3. 开发小技巧系统 - Java实现树形结构的方式有那些?

4. 开发小技巧系列 - 如何避免NullPointerException?(一)