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

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

先来回答上篇文章中“思考”的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的出现。

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

                                    https://gitee.com/TianXiaoSe_admin/java-npe-demo

                                    题外话,可能在开发过程中,经常需要去判断属性是否有值,没值要给个默认值,比如如下的代码,基本上是三元表过式(这是比较的方法,而大部分初入开发,可能都是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);....


                                      更多内容与交流,欢迎关注公众号

                                      111111.png

                                      开发小技巧系列文章:

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

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

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

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

                                      本文链接:https://www.jhelp.net/p/VmVaA0xHNEsDdgAk (转载请保留)。
                                      关注下面的标签,发现更多相似文章