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

简介: NPE是一个老生长谈的问题,无论新手,还是老手,在开发程序的过程中,都不可避免会遇到,而为了处理NPE,往往需要添加很多重复性的检查代码,又长又臭。NPE系列文章,是总结了过往的开发经验,助力更多新手,避免踩坑。

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

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?(二)

目录
相关文章
|
2月前
|
Java
巧用枚举消除条件判断
`shigen`是一位致力于撰写博客文章的作者,记录个人成长历程,分享真知灼见,并捕捉生活中的感动瞬间。在其最新文章中,通过枚举的方式展示了如何优雅地消除if-else判断,提供了一种更为简洁清晰的代码实现方案。利用自定义的`QuestionHandlerEnum`枚举类,实现了不同情况下的逻辑处理,展示了此方法在提升代码可读性和内聚性上的优势。与`shigen`一同探索编程之美,让每一天都有所进步。个人IP:shigen。
13 0
巧用枚举消除条件判断
|
4月前
|
Java
代码优化设计问题之推荐使用函数式方法进行null判断问题如何解决
代码优化设计问题之推荐使用函数式方法进行null判断问题如何解决
|
6月前
|
Java
Java【问题记录 03】三目运算符失效问题刨根问底(及NPE分析)
Java【问题记录 03】三目运算符失效问题刨根问底(及NPE分析)
62 1
|
程序员
相见恨晚的Matlab编程小技巧(2)-代码怎么做到逻辑清晰?——巧用注释符“%“
        本文将以教程的形式详细介绍Matlab中两个常用符号“%”和“%%”的作用。初学者可以通过此文掌握这两个符号的用法,为Matlab编程打下坚实的基础。
|
Java 测试技术 API
开发小技巧系列 - 如何避免NPE,去掉if...else(四)
利用optional来处理各种IF-ELSE的判断
107 0
|
Dart Java Kotlin
|
移动开发 缓存 HTML5
是时候学习/推广一波可选链(Optional chaining)和空值合并(Nullish coalescing )了
最近工作中发现团队有些同学不太了解 Optional chaining 和 Nullish coalescing 两个新的操作符,正好推广一波。
是时候学习/推广一波可选链(Optional chaining)和空值合并(Nullish coalescing )了
C++中逻辑操作符的陷阱
C++中逻辑操作符的陷阱
70 0
|
消息中间件 监控 前端开发
生产环境一次诡异的NPE问题,反转了4次
生产环境一次诡异的NPE问题,反转了4次
生产环境一次诡异的NPE问题,反转了4次
|
缓存 小程序 Java
IntegerCache的妙用和陷阱!
考虑下面的小程序,你认为会输出为什么结果?
121 0
IntegerCache的妙用和陷阱!