javaset空值_Java中如何更优雅的处理空值
经常看到项⽬中存在到处空值判断的情况,这些判断,会让⼈觉得摸不着头绪,它的出现很有可能和当前的业务逻辑并没有关系。但它会让你很头疼。有时候,更可怕的是系统因为这些空值的情况,会抛出空指针异常,导致业务系统发⽣问题。
此篇⽂章总结了⼏种关于空值的处理⼿法
业务中的空值
场景
存在⼀个UserSearchService⽤来提供⽤户查询的功能:
public interfaceUserSearchService{
ListlistUser();
Userget(Integer id);
}
问题现场
对于⾯向对象语⾔来讲,抽象层级特别的重要。尤其是对接⼝的抽象,它在设计和开发中占很⼤的⽐重,我们在开发时希望尽量⾯向接⼝编程。
对于以上描述的接⼝⽅法来看,⼤概可以推断出可能它包含了以下两个含义:
listUser(): 查询⽤户列表
get(Integer id): 查询单个⽤户
在所有的开发中,XP推崇的TDD模式可以很好的引导我们对接⼝的定义,所以我们将TDD作为开发代码的”推动者”。
对于以上的接⼝,当我们使⽤TDD进⾏测试⽤例先⾏时,发现了潜在的问题:
listUser() 如果没有数据,那它是返回空集合还是null呢?
get(Integer id) 如果没有这个对象,是抛异常还是返回null呢?
深⼊listUser研究
listUser()
这个接⼝,我经常看到如下实现:
public ListlistUser(){
List userList = userListRepostity.selectByExample(newUserExample());if(CollectionUtils.isEmpty(userList)){//spring util⼯具类
return null;
}returnuserList;
}
这段代码返回是null,对于集合这样返回值,最好不要返回null,因为如果返回了null,会给调⽤者带来很多⿇烦。你将会把这种调⽤风险交给调⽤者来控制。
如果调⽤者是⼀个谨慎的⼈,他会进⾏是否为null的条件判断。如果他并⾮谨慎,他会按照⾃⼰的理解
去调⽤接⼝,⽽不进⾏是否为null的条件判断,如果这样的话,是⾮常危险的,它很有可能出现空指针异常!
根据墨菲定律来判断: “很有可能出现的问题,在将来⼀定会出现!”
基于此,我们将它进⾏优化:
public ListlistUser(){
List userList = userListRepostity.selectByExample(newUserExample());if(CollectionUtils.isEmpty(userList)){return
}returnuserList;
}
对于接⼝(List listUser()),它⼀定会返回List,即使没有数据,它仍然会返回List(集合中没有任何元素);
通过以上的修改,我们成功的避免了有可能发⽣的空指针异常,这样的写法更安全!
深⼊研究get⽅法
对于接⼝
User get(Integer id)
你能看到的现象是,我给出id,它⼀定会给我返回User.但事实真的很有可能不是这样的。
我看到过的实现:
public User get(Integer id){return userRepository.selectByPrimaryKey(id);//从数据库中通过id直接获取实体对象
}
相信很多⼈也都会这样写。
通过代码的时候得知它的返回值很有可能是null! 但我们通过的接⼝是分辨不出来的!
这个是个⾮常危险的事情。尤其对于调⽤者来说!
建议需要在接⼝明明时补充⽂档,⽐如对于异常的说明,使⽤注解@exception:
public interfaceUserSearchService{/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
* @return ⽤户实体
* @exception UserNotFoundException*/Userget(Integer id);
}
我们把接⼝定义加上了说明之后,调⽤者会看到,如果调⽤此接⼝,很有可能抛出“UserNotFoundException(不到⽤户)”这样的异常。
这种⽅式可以在调⽤者调⽤接⼝的时候看到接⼝的定义,但是,这种⽅式是”弱提⽰”的!
如果调⽤者忽略了注释,有可能就对业务系统产⽣了风险,这个风险有可能导致⼀个亿!
除了以上这种”弱提⽰”的⽅式,还有⼀种⽅式是,返回值是有可能为空的。那要怎么办呢?
我认为我们需要增加⼀个接⼝,⽤来描述这种场景.
引⼊jdk8的Optional,或者使⽤guava的Optional.看如下定义:
public interfaceUserSearchService{/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
* @return ⽤户实体,此实体有可能是缺省值*/OptionalgetOptional(Integer id);
}
Optional有两个含义: 存在 or 缺省。
那么通过阅读接⼝getOptional(),我们可以很快的了解返回值的意图,这个其实是我们想看到的,它去除了⼆义性。
它的实现可以写成:
public OptionalgetOptional(Integer id){returnOptional.ofNullable(userRepository.selectByPrimaryKey(id));
}
深⼊⼊参
通过上述的所有接⼝的描述,你能确定⼊参id⼀定是必传的吗?答案应该是:不能确定。除⾮接⼝的⽂档注释上加以说明。那如何约束⼊参呢?
给⼤家推荐两种⽅式:
强制约束
⽂档性约束(弱提⽰)
1.强制约束,我们可以通过jsr 303进⾏严格的约束声明:
public interfaceUserSearchService{/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
* @return ⽤户实体
* @exception UserNotFoundException*/Userget(@NotNull Integer id);/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
* @return ⽤户实体,此实体有可能是缺省值*/OptionalgetOptional(@NotNull Integer id);
}
当然,这样写,要配合AOP的操作进⾏验证,但让spring已经提供了很好的集成⽅案,在此我就不在赘述了。
2.⽂档性约束
在很多时候,我们会遇到遗留代码,对于遗留代码,整体性改造的可能性很⼩。
我们更希望通过阅读接⼝的实现,来进⾏接⼝的说明。
jsr 305规范,给了我们⼀个描述接⼝⼊参的⼀个⽅式(需要引⼊库 de.findbugs:jsr305):
可以使⽤注解: @Nullable @Nonnull @CheckForNull 进⾏接⼝说明。⽐如:
public interfaceUserSearchService{/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
jdk怎么使用* @return ⽤户实体
* @exception UserNotFoundException*/@CheckForNull
Userget(@NonNull Integer id);/**
* 根据⽤户id获取⽤户信息
* @param id ⽤户id
* @return ⽤户实体,此实体有可能是缺省值*/OptionalgetOptional(@NonNull Integer id);
}
⼩结
通过 空集合返回值,Optional,jsr 303,jsr 305这⼏种⽅式,可以让我们的代码可读性更强,出错率更低!
空集合返回值 :如果有集合这样返回值时,除⾮真的有说服⾃⼰的理由,否则,⼀定要返回空集合,⽽不是null
Optional: 如果你的代码是jdk8,就引⼊它!如果不是,则使⽤Guava的Optional,或者升级jdk版本!它很⼤程度的能增加了接⼝的可读性!
jsr 303: 如果新的项⽬正在开发,不防加上这个试试!⼀定有⼀种特别爽的感觉!
jsr 305: 如果⽼的项⽬在你的⼿上,你可以尝试的加上这种⽂档型注解,有助于你后期的重构,或者新功能增加了,对于⽼接⼝的理解!
空对象模式
场景
我们来看⼀个DTO转化的场景,对象:
@Datastatic classPersonDTO{privateString dtoName;privateString dtoAge;
}
@Datastatic classPerson{privateString name;privateString age;
}
需求是将Person对象转化成PersonDTO,然后进⾏返回。
当然对于实际操作来讲,返回如果Person为空,将返回null,但是PersonDTO是不能返回null的(尤其Rest接⼝返回的这种DTO)。
在这⾥,我们只关注转化操作,看如下代码:
@Testpublic voidshouldConvertDTO(){
PersonDTO personDTO= newPersonDTO();
Person person= newPerson();if(!Objects.isNull(person)){
personDTO.Age());
personDTO.Name());
}else{
personDTO.setDtoAge("");
personDTO.setDtoName("");
}
}
优化修改
这样的数据转化,我们认识可读性⾮常差,每个字段的判断,如果是空就设置为空字符串(“”)
换⼀种思维⽅式进⾏思考,我们是拿到Person这个类的数据,然后进⾏赋值操作(setXXX),其实是不关系Person的具体实现是谁的。
那我们可以创建⼀个Person⼦类:
static classNullPerson extends Person{
@OverridepublicString getAge() {return "";
}
@OverridepublicString getName() {return "";
}
}
它作为Person的⼀种特例⽽存在,如果当Person为空的时候,则返回⼀些get*的默认⾏为.
所以代码可以修改为:
@Testpublic voidshouldConvertDTO(){
PersonDTO personDTO= newPersonDTO();
Person person=getPerson();
personDTO.Age());
personDTO.Name());
}privatePerson getPerson(){return new NullPerson();//如果Person是null ,则返回空对象
}
其中getPerson()⽅法,可以⽤来根据业务逻辑获取Person有可能的对象(对当前例⼦来讲,如果Person不存在,返回Person的的特例NUllPerson),如果修改成这样,代码的可读性就会变的很强了。
使⽤Optional可以进⾏优化
空对象模式,它的弊端在于需要创建⼀个特例对象,但是如果特例的情况⽐较多,我们是不是需要创建多个特例对象呢,虽然我们也使⽤了⾯向对象的多态特性,但是,业务的复杂性如果真的让我们创建多个特例对象,我们还是要再三考虑⼀下这种模式,它可能会带来代码的复杂性。
对于上述代码,还可以使⽤Optional进⾏优化。
@Testpublic voidshouldConvertDTO(){
PersonDTO personDTO= newPersonDTO();
Optional.ofNullable(getPerson()).ifPresent(person->{
personDTO.Age());
personDTO.Name());
});
}privatePerson getPerson(){return null;
}
Optional对空值的使⽤,我觉得更为贴切,它只适⽤于”是否存在”的场景。
如果只对控制的存在判断,我建议使⽤Optional.
Optioanl的正确使⽤
Optional如此强⼤,它表达了计算机最原始的特性(0 or 1),那它如何正确的被使⽤呢!