开发小技巧系列文章,是本人对过往平台系统的设计开发及踩坑的记录与总结,给初入平台系统开发的开发人员提供参考与帮助。
NPE是一个老生长谈的问题,无论新手,还是老手,在开发程序的过程中,都不可避免会遇到,而为了处理NPE,往往需要添加很多重复性的检查代码,又长又臭。NPE系列文章,是总结了过往的开发经验,助力更多新手,避免踩坑。这是第三篇,没有看过前二篇的,可以访问以下链接。
开发小技巧系列 - 如何避免NullPointerException?(一)
开发小技巧系列 - 如何避免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?(二)