开发小技巧系列文章,是本人对过往平台系统的设计开发及踩坑的记录与总结,给初入平台系统开发的开发人员提供参考与帮助。
先来回答上篇文章中“思考”的2个问题。1、 这段程序为什么不用“==”号了? “==”号在数字较大时会有什么问题?答:在对象类型中,使用“==”时,一般是比较的地址,而不是具体的值,当然Integer(int)有点除外,它会将-128~127的值缓存起来,在这个范围内,使用“==”进行比较,是没有问题,但是超过这个范围,使用“==”号,就会返回不是预期的效果。因此,在程序中,尽量使用equals,来避免埋下的坑。(建议使用Objects.equals),这样可以防止xx.equals(...)表达式中的 xx 为NPE的情况。
2、 @NotNull 有什么作用?答:这个@NotNull起到修饰说明的作用,提醒使用者,注意传入的参数值,对程序运行不起作用。悬浮时会有如下的提示:
案例二:
在开发的过程中,不可避免地需要从对象中获取属性的值,比如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);....
更多内容与交流,欢迎关注公众号
开发小技巧系列文章: