开发小技巧系列 - 如何避免NPE,巧用Optional重构三元表达式?(三)

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

NPE是一个老生长谈的问题,无论新手,还是老手,在开发程序的过程中,都不可避免会遇到,而为了处理NPE,往往需要添加很多重复性的检查代码,又长又臭。NPE系列文章,是总结了过往的开发经验,助力更多新手,避免踩坑。这是第三篇,没有看过前二篇的,可以访问以下链接。

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

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

在第二篇的结束时,提出了一个问题,有时候在程序中需要读取某些数值,然后在数值为null时,赋一个默认值,这时候,大部分的写法是三元表达式,相对if来来,程序更简洁。比如以下的代码场景:

    //假设有一个销售数据的对象(里面有订单金额,订单量,交易金额,成交商品数,成交客户数,客单值等
    //需木给前端返回值对象(如是NULL,则返回0)
    //可能的代码会是如下:
    //销售数据对象SellDataInfo sellDataInfo = ...;
    //返回给前端的DTO
    RevenueIndicatorDTO 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);
    ....

    那么,针对程序中大量的三元表达式,有没有更好的处理方法,来减少 null ! =obj这样的写法呢?当然是有的。

    可以利用jdk1.8提供的Optional特性,来简化对null的判断,可以编写一个模板方法,统一的模板方法,可以应用到项目中的其他取数逻辑上,也方便后期的统一维护,而不是每个人自己写一块自有的判断逻辑

    ValueUtils.java

          /**
           * 转换null值的模板方法
           * @param value
           *  输入的值
           * @param defaultValue
           *  默认值
           * @param <R> 
           *     输入的对象的类型
           * @return
           */
          public static <R> R wrapNull(R value, R defaultValue){
              Optional optional = Optional.ofNullable(value);
              if(optional.isPresent()){
                  return value;
              }
              return defaultValue;
          }
      /** * 转换BigDecimal(浮点数)数值可能为null的情况 * @param value * 输入数值 * @param scale * 指定小数位 * @param defaultValue * 指定默认值 * @return * 默认四舍五入 * 如果为null,则返回 defaultValue */ public static BigDecimal wrapNull(BigDecimal value, int scale, BigDecimal defaultValue){ Optional optional = Optional.ofNullable(value); if(optional.isPresent()){ return value.setScale(scale, BigDecimal.ROUND_HALF_UP); } return defaultValue; }

      模板方法写好,来看看前后调整效果的对照。

      假设有个数据对象是:OrderSaleData.java,

        @Data
        @Accessors(chain = true)
        public class OrderSaleData {
        /** * 商家id */ private Integer shopId;
        /** * 统计日期 */ private Date logDate;
        /** * 订单数 */ private Integer orderCount;
        /** * 订单销售金额 */ private BigDecimal orderAmount;
        /** * GMV 金额 */ private BigDecimal orderGmvAmount;
        /** * 买家数量 */ private Integer buyerCount;
        /** * 销售sku件数 */ private Integer saleSkuCount;
        /** * 平均客单价 */ private BigDecimal avgBuyerPrice; }


        输出对象是:ShopSaleDataDTO.java

          @Data
          public class ShopSaleDataDTO {
              /**
               * 商家id
               */
              private Integer shopId;
              /**
               *  统计日期
               */
              private String logDate;
              /**
               * 订单数
               */
              private Integer orderCount;
              /**
               * 订单销售金额
               */
              private BigDecimal orderAmount;
              /**
               * GMV 金额
               */
              private BigDecimal orderGmvAmount;
              /**
               * 买家数量
               */
              private Integer buyerCount;
              /**
               * 销售sku件数
               */
              private Integer saleSkuCount;
              /**
               * 平均客单价
               */
              private BigDecimal avgBuyerPrice;


          测试类:NullValueTest.java

                @Test
                public void nullValueTest(){
                    OrderSaleData orderSaleData = new OrderSaleData()
                            .setShopId(1000)
                            .setLogDate(new java.util.Date())
                            .setOrderAmount(new BigDecimal(988.1234))
                            .setOrderCount(100)
                            .setOrderGmvAmount(new BigDecimal(2012.1265))
                            .setSaleSkuCount(300)
                            .setBuyerCount(34)
                            .setAvgBuyerPrice(new BigDecimal(988.1234).divide(new BigDecimal(34)));
            
            ShopSaleDataDTO dto1 = toDTO1(orderSaleData); log.debug("传统方法的结果:{}", dto1); ShopSaleDataDTO dto2 = toDTO2(orderSaleData); log.debug("Optional模板方法的结果:{}", dto2);    }     /**   * 比较常见的赋值过程 , * 通过判断值是否为null,或者三元式来处理 * @param orderSaleData * @return */ private ShopSaleDataDTO toDTO1(OrderSaleData orderSaleData){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); ShopSaleDataDTO shopSaleDataDTO = new ShopSaleDataDTO(); shopSaleDataDTO.setShopId(orderSaleData.getShopId()); shopSaleDataDTO.setLogDate(orderSaleData.getLogDate() == null ? "" : sdf.format(orderSaleData.getLogDate()));
            shopSaleDataDTO.setBuyerCount(orderSaleData.getBuyerCount() == null ? 0 : orderSaleData.getBuyerCount()); shopSaleDataDTO.setOrderCount(orderSaleData.getOrderCount() == null ? 0 : orderSaleData.getOrderCount()); shopSaleDataDTO.setOrderAmount(orderSaleData.getOrderAmount() == null ? BigDecimal.ZERO : orderSaleData.getOrderAmount().setScale(2, BigDecimal.ROUND_HALF_UP)); shopSaleDataDTO.setSaleSkuCount(orderSaleData.getSaleSkuCount() == null ? 0 : orderSaleData.getSaleSkuCount()); shopSaleDataDTO.setOrderGmvAmount(orderSaleData.getOrderGmvAmount() == null ? BigDecimal.ZERO : orderSaleData.getOrderGmvAmount().setScale(2, BigDecimal.ROUND_HALF_UP)); shopSaleDataDTO.setAvgBuyerPrice(orderSaleData.getAvgBuyerPrice() == null ? BigDecimal.ZERO : orderSaleData.getAvgBuyerPrice().setScale(2, BigDecimal.ROUND_HALF_UP)); return shopSaleDataDTO; }
            /** * 通过Optional的特性,定义模板方法来抽象对 obj == null的重复代码 * @param orderSaleData * @return */ private ShopSaleDataDTO toDTO2(OrderSaleData orderSaleData){ ShopSaleDataDTO shopSaleDataDTO = new ShopSaleDataDTO(); //使用Optional模板方法 shopSaleDataDTO.setShopId(orderSaleData.getShopId()); shopSaleDataDTO.setLogDate(ValueUtils.formatDate(orderSaleData.getLogDate(), "")); shopSaleDataDTO.setOrderCount(ValueUtils.wrapNull(orderSaleData.getOrderCount(), 0)); shopSaleDataDTO.setBuyerCount(ValueUtils.wrapNull(orderSaleData.getBuyerCount(), 0)); shopSaleDataDTO.setSaleSkuCount(ValueUtils.wrapNull(orderSaleData.getSaleSkuCount(), 0)); shopSaleDataDTO.setOrderAmount(ValueUtils.wrapNull(orderSaleData.getOrderAmount(), 2, BigDecimal.ZERO)); shopSaleDataDTO.setOrderGmvAmount(ValueUtils.wrapNull(orderSaleData.getOrderGmvAmount(), 2, BigDecimal.ZERO)); shopSaleDataDTO.setAvgBuyerPrice(ValueUtils.wrapNull(orderSaleData.getAvgBuyerPrice(), 2, BigDecimal.ZERO)); return shopSaleDataDTO; }

            从上面的测试用例中的方法,可以看出,toDTO2 比 toDTO1简单化了对数值对象的NULL判断,只是一个简单的方法,这样,在其他地方,如果需要对整数,长整,浮点数的处理场景下,都可以使用ValueUtils中的方法,也使用程序简洁。

            那么上面的程序还能不能再优化下?其实,还是可以进一步简化,可以对 ValueUtils中的方法,进一步优化,将模板方法,拆分为不同类型的方法,利用方法的多态性。这样也可实现按需来设置不同的赋值需求。调整后的代码

            ValueUtils.java

                /**
                   * 转换BigDecimal(浮点数)数值可能为null的情况
                   * @param value
                   *   输入数值
                   * @return
                   *      默认返回2位数,四舍五入
                   *      如果为null,则返回0 
                  */
                  public static BigDecimal wrapNull(BigDecimal value){
                      return wrapNull(value, 2, BigDecimal.ZERO);
                  }
              /** * 转换BigDecimal(浮点数)数值可能为null的情况,如果为null,则返回0 * @param value * 输入数值 * @param scale * 指定小数位 * @return * 默认四舍五入 * 如果为null,则返回0 */ public static BigDecimal wrapNull(BigDecimal value, int scale){ return wrapNull(value, scale, BigDecimal.ZERO); }
              /** * 转换BigDecimal(浮点数)数值可能为null的情况 * @param value * 输入数值 * @param scale * 指定小数位 * @param defaultValue * 指定默认值 * @return * 默认四舍五入 * 如果为null,则返回 defaultValue */ public static BigDecimal wrapNull(BigDecimal value, int scale, BigDecimal defaultValue){ Optional optional = Optional.ofNullable(value); if(optional.isPresent()){ return value.setScale(scale, BigDecimal.ROUND_HALF_UP); } return defaultValue; }
              /** * 转换输入的整数为null的情况 * @param input * 输入数值 * @return * 如果是null,则返回0 */ public static Integer nullInt(Integer input){ return nullInt(input, 0); }
              /** * 转换输入的整数为null的情况 * @param input * 输入数值 * @param defaultValue * 指定默认值 * @return * 如果是null,则返回 defaultValue */ public static Integer nullInt(Integer input, Integer defaultValue){ Optional optional = Optional.ofNullable(input); if(optional.isPresent()){ return input; } return defaultValue; }
              /** * 转换输入的长整数为null的情况 * @param input * 输入数值 * @return * 如果是null,则返回0 */ public static Long nullLong(Long input){ return nullLong(input, 0L); }
              /** * 转换输入的长整数为null的情况 * @param input * 输入数值 * @param defaultValue * 指定默认值 * @return * 如果是null,则返回 defaultValue */ public static Long nullLong(Long input, Long defaultValue){ Optional optional = Optional.ofNullable(input); if(optional.isPresent()){ return input; } return defaultValue; }
              /** * 转换输入的字符串为null的情况 * @param input * 输入数值 * @return * 如果是null,则返回空("") */ public static String nullString(String input){ return nullString(input, ""); }
              /** * 转换输入的字符串为null的情况 * @param input * 输入数值 * @param defaultValue * 指定默认值 * @return * 如果是null,则返回 defaultValue */ public static String nullString(String input, String defaultValue){ Optional optional = Optional.ofNullable(input); if(optional.isPresent()){ return input.trim(); } return defaultValue; }
              /** * 格式化BigDecimal(浮点数)为字符串 * @param input * 输入数值 * @return * 默认返回2位小数,四舍五入 * 如果是null,则返回("0.00") */ public static String formatBigDecimal(BigDecimal input){ return formatBigDecimal(input, 2); }
              /** * 格式化BigDecimal(浮点数)为字符串 * @param input * 输入数值 * @param scale * 指定小数位 * @return * 默认 四舍五入 * 如果是null,则返回("0.00") */ public static String formatBigDecimal(BigDecimal input, int scale){ Optional optional = Optional.ofNullable(input); if(optional.isPresent()){ return String.valueOf(input.setScale(scale, BigDecimal.ROUND_HALF_UP)); } return "0.00"; }
              /** * 格式化Date(日期)为字符串(yyyy-MM-dd) * @param input * 输入数值 * @param pattern * 日期格式,如yyyy-MM-dd * @return * 如果是null,则返回空("") */ public static String formatDate(Date input, String pattern){ return formatDate(input, pattern, ""); }
              /** * 格式化Date(日期)为字符串 * @param input * 输入数值 * @param pattern * 日期格式,如yyyy-MM-dd * @param defaultDate * 指定的默认值 * @return * 如果是null,则返回 defaultDate */ public static String formatDate(Date input, String pattern, String defaultDate){ Optional optional = Optional.ofNullable(input); if(optional.isPresent()){ SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); return simpleDateFormat.format(input); } return defaultDate; }

              根据上面的调整结果,在测试用例中,添加新的取数处理逻辑。NullValueTest.java

                @Test
                    public void nullValueTest(){
                        OrderSaleData orderSaleData = new OrderSaleData()
                                .setShopId(1000)
                                .setLogDate(new java.util.Date())
                                .setOrderAmount(new BigDecimal(988.1234))
                                .setOrderCount(100)
                                .setOrderGmvAmount(new BigDecimal(2012.1265))
                                .setSaleSkuCount(300)
                                .setBuyerCount(34)
                                .setAvgBuyerPrice(new BigDecimal(988.1234).divide(new BigDecimal(34)));
                
                ShopSaleDataDTO dto1 = toDTO1(orderSaleData); log.debug("传统方法的结果:{}", dto1);
                ShopSaleDataDTO dto2 = toDTO2(orderSaleData); log.debug("Optional模板方法的结果:{}", dto2);
                ShopSaleDataDTO dto3 = toDTO3(orderSaleData); log.debug("Optional+多元化方法的结果:{}", dto3);
                } /** * 通过对null转换的方法的改造(多元化),实现更简洁的代码 * * @param orderSaleData * @return */ private ShopSaleDataDTO toDTO3(OrderSaleData orderSaleData){ ShopSaleDataDTO shopSaleDataDTO = new ShopSaleDataDTO(); //更简单的方式 shopSaleDataDTO.setShopId(orderSaleData.getShopId()); shopSaleDataDTO.setLogDate(ValueUtils.formatDate(orderSaleData.getLogDate(), "yyyy-MM-dd")); shopSaleDataDTO.setOrderCount(ValueUtils.nullInt(orderSaleData.getOrderCount())); shopSaleDataDTO.setBuyerCount(ValueUtils.nullInt(orderSaleData.getBuyerCount())); shopSaleDataDTO.setSaleSkuCount(ValueUtils.nullInt(orderSaleData.getSaleSkuCount())); shopSaleDataDTO.setOrderAmount(ValueUtils.wrapNull(orderSaleData.getOrderAmount())); shopSaleDataDTO.setOrderGmvAmount(ValueUtils.wrapNull(orderSaleData.getOrderGmvAmount())); shopSaleDataDTO.setAvgBuyerPrice(ValueUtils.wrapNull(orderSaleData.getAvgBuyerPrice())); return shopSaleDataDTO; }

                运行这个测试用例,来看下结果:

                  11:56:31.502 [main] DEBUG net.jhelp.demo.NullValueTest - 传统方法结果:ShopSaleDataDTO(shopId=1000, logDate=2022-04-19, orderCount=100, orderAmount=988.12, orderGmvAmount=2012.12, buyerCount=34, saleSkuCount=300, avgBuyerPrice=29.06)
                  11:56:31.519 [main] DEBUG net.jhelp.demo.NullValueTest - Optional模板方法结果:ShopSaleDataDTO(shopId=1000, logDate=, orderCount=100, orderAmount=988.12, orderGmvAmount=2012.12, buyerCount=34, saleSkuCount=300, avgBuyerPrice=29.06)
                  11:56:31.520 [main] DEBUG net.jhelp.demo.NullValueTest - Optional+多元化方法结果:ShopSaleDataDTO(shopId=1000, logDate=2022-04-19, orderCount=100, orderAmount=988.12, orderGmvAmount=2012.12, buyerCount=34, saleSkuCount=300, avgBuyerPrice=29.06)

                  从结果来看,三种方式的运行结果都是一样。

                  总结一下,通过上面的例子的推演,只需要在项目中添加一个通用的工具类,就可以简化程序中对Null的判断(三元表达式或者if代码块的使用)。最后方法的代码是不是看起来舒服多了。

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

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

                  思考一下:上面的程序中,为什么使用的是BigDecimal,而不是使用Double, Float呢?在计算金额时,Double, Float有什么问题呢? 静等下回分析。

                  方法(思考)比结果重要,希望你从中能有所收获,不浪费码这么多字。

                  开发小技巧系列文章:

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

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

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

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

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

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