导语
作为一位Java研发,饱受了NullPointerException摧残。一方面如果不做null的判断,程序可能出现NullPointerException,另有一方如果做null判断,这些判断让开发起来感到奔溃,也让后来阅读人摸不着头脑。对应Java中的空指针,小编只想说一句,无论你处不处理,它都在那里,不离不弃,只为摧残你!
业务中的空值
一般我们开发过程经常用接收的两种返回值,集合和实体对象,例如我们通常使用的定义的接口
public interface UserSearchService{
List<User> listUser();
User get(Integer id);
}
listUser()经常遇到的实现方案
public List<User> listUser(){
List<User> userList = userDao.selectAll();
if(CollectionUtils.isEmpty(userList)){
return null;
}
return userList;
}
这段代码返回情况两种null和集合,集合就是正常返回内容,特殊的是null值。如果调用不对返回值进行判断,那么恭喜调用者等待接收NullPointerException吧。从开发角度来说,如果接口返回null,会给调用者带来潜在风险,尤其是对方经验尚浅的情况,这种将调用风险交给调用者来控制的方式,通常不可取。一般面对返回集合的这种情况,即便查询内容为null,建议分组成空集合返回。即为下面的方式
public List<User> listUser(){
List<User> userList = userListDao.selectAll();
if(CollectionUtils.isEmpty(userList)){
return Lists.newArrayList();//guava类库提供的方式
}
return userList;
}
对于接口(List listUser())
,它一定会返回List,即使没有数据,它仍然会返回List(集合中没有任何元素);
通过以上的修改,我们成功的避免了有可能发生的空指针异常,这样的写法更安全!
User get(Integer id)通常的实现方案为
public User get(Integer id){
return userDao.selectById(id);//从数据库中通过id直接获取实体对象
}
从上述逻辑可以判断,调用方接收为数据库的直接返回值,不一定是user对象,很有可能是null值,但是这种情况调用者通常分辨不出来。为了方便调用者的使用,通常建议采用以下几种方案
1、弱提示,在接口补充文档,标注可能出现的异常
public interface UserSearchService{
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体
* @exception UserNotFoundException
*/
User get(Integer id);
}
2、利用Java8中Optional,或者使用guava中的Optional
public interface UserSearchService{
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体,此实体有可能是缺省值
*/
Optional<User> getOptional(Integer id);
}
Optional有两种含义:存在或缺省
通过阅读上边Optional的方法,很快就知道返回值的具体的内容,它告诉调用者,返回值有两种可能性,需要进行判定
具体的实现内容
public Optional<User> getOptional(Integer id){
return Optional.ofNullable(userRepository.selectByPrimaryKey(id));
}
3、返回空对象,即利用空对象模式
如果user对象需要转成DTO,实际操作将返回null值,通常来说DTO是不能返回null值的尤其Rest接口返回的DTO
public void shouldConvertDTO(){
PersonDTO personDTO = new PersonDTO();
Person person = new Person();
if(!Objects.isNull(person)){
personDTO.setDtoAge(person.getAge());
personDTO.setDtoName(person.getName());
}else{
personDTO.setDtoAge("");
personDTO.setDtoName("");
}
}
这样的数据转化,可读性较差,而且很有可能造成代码冗余,此时可以利用空对象模式来处理,本质只关注拿到存在的对象即可,不关心具体的对象是谁
static class NullPerson extends Person{
@Override
public String getAge() {
return "";
}
@Override
public String getName() {
return "";
}
}
它作为Person的一种特例而存在,如果当Person为空的时候,则返回一些get*的默认行为
代码优化后为
public void shouldConvertDTO(){
PersonDTO personDTO = new PersonDTO();
Person person = getPerson();
personDTO.setDtoAge(person.getAge());
personDTO.setDtoName(person.getName());
}
private Person getPerson(){
return new NullPerson();//如果Person是null ,则返回空对象
}
其中getPerson()方法,可以用来根据业务逻辑获取Person有可能的对象(对当前例子来讲,如果Person不存在,返回Person的的特例NUllPerson),如果修改成这样,代码的可读性就会变的很强了。
空对象模式,弊端在于需要创建一个特例对象,如果业务复杂度需要让我们创建多个特例对象,这有可能带来代码的复杂性,使用的时候需要三思而用。
可以利用Optional进行优化
public void shouldConvertDTO(){
PersonDTO personDTO = new PersonDTO();
Optional.ofNullable(getPerson()).ifPresent(person -> {
personDTO.setDtoAge(person.getAge());
personDTO.setDtoName(person.getName());
});
}
private Person getPerson(){
return null;
}
但是这种调用接收到的是空对象,但是空对象是嵌套的还要子对象,依旧有空指针的风险,不如使用空对象模式靠谱,但是方便简洁。
关于入参
public interface UserSearchService{
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体,此实体有可能是缺省值
*/
Optional<User> getOptional(Integer id);
}
仅看接口文档,不能确定入参是否为必填项,同样存在 NullPointerException的风险。如何约束入参呢?其实和返回值处理方式基本相同
强制约束
文档性约束(弱提示)
强制约束,利用jsr 303进行严格的约束声明
public interface UserSearchService{
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体,此实体有可能是缺省值
*/
Optional<User> getOptional(@NotNull Integer id);
}
文档性约束
在很多时候,我们会遇到遗留代码,对于遗留代码,整体性改造的可能性很小。
我们更希望通过阅读接口的实现,来进行接口的说明。
jsr 305规范,给了我们一个描述接口入参的一个方式(需要引入库 com.google.code.findbugs:jsr305):
可以使用注解: @Nullable @Nonnull @CheckForNull 进行接口说明
public interface UserSearchService{
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体
* @exception UserNotFoundException
*/
@CheckForNull
User get(@NonNull Integer id);
/**
* 根据用户id获取用户信息
* @param id 用户id
* @return 用户实体,此实体有可能是缺省值
*/
Optional<User> getOptional(@NonNull Integer id);
}
题外话——Optional
关于Optional的使用不要作为参数,因为容易产生歧义
public interface UserService{
List<User> listUser(Optional<String> username);
}
“如果username是absent,是返回空集合吗?还是返回全部的用户数据集合?”
如果你真的想表达两个含义,就給它拆分出两个接口,这样更能满足设计原则中“单一职责”
public interface UserService{
List<User> listUser(String username);
List<User> listUser();
}
Optional作为返回值
当个实体的返回
那Optioanl可以做为返回值吗?
其实它是非常满足是否存在这个语义的。
你如说,你要根据id获取用户信息,这个用户有可能存在或者不存在。
你可以这样使用:
public interface UserService{
Optional<User> get(Integer id);
}
当调用这个方法的时候,调用者很清楚get方法返回的数据,有可能不存在,这样可以做一些更合理的判断,更好的防止空指针的错误!
当然,如果业务方真的需要根据id必须查询出User的话,就不要这样使用了,请说明,你要抛出的异常.
只有当考虑它返回null是合理的情况下,才进行Optional的返回
集合实体的返回
不是所有的返回值都可以这样用的!如果你返回的是集合:
public interface UserService{
Optional<List<User>> listUser();
}
这样的返回结果,会让调用者不知所措,是否我判断Optional之后,还用进行isEmpty的判断呢?
这样带来的返回值歧义!我认为是没有必要的。
我们要约定,对于List这种集合返回值,如果集合真的是null的,请返回空集合(Lists.newArrayList);
使用Optional变量
Optional<User> userOpt = ...
如果有这样的变量userOpt,请记住 :
-
一定不能直接使用get ,如果这样用,就丧失了Optional本身的含义 ( 比如userOp.get() )
-
不要直接使用getOrThrow ,如果你有这样的需求:获取不到就抛异常。那就要考虑,是否是调用的接口设计的是否合理
getter中的使用
对于一个java bean,所有的属性都有可能返回null,那是否需要改写所有的getter成为Optional类型呢?
不要这样滥用Optional.
即便 java bean中的getter是符合Optional的,但是因为java bean 太多了,这样会导致你的代码有50%以上进行Optinal的判断,这样便污染了代码。(其实你的实体中的字段应该都是由业务含义的,会认真的思考过它存在的价值的,不能因为Optional的存在而滥用)
应该更关注于业务,而不只是空值的判断。
总结
通过 空集合返回值,Optional,jsr 303,jsr 305这几种方式,可以让我们的代码可读性更强,出错率更低!
-
空集合返回值 :如果有集合这样返回值时,除非真的有说服自己的理由,否则,一定要返回空集合,而不是null
-
Optional: 如果你的代码是jdk8,就引入它!如果不是,则使用Guava的Optional,或者升级jdk版本!它很大程度的能增加了接口的可读性!
-
jsr 303: 如果新的项目正在开发,不防加上这个试试!一定有一种特别爽的感觉!
-
jsr 305: 如果老的项目在你的手上,你可以尝试的加上这种文档型注解,有助于你后期的重构,或者新功能增加了,对于老接口的理解!
总结Optional的使用:
-
当使用值为空的情况,并非源于错误时,可以使用Optional!
-
Optional不要用于集合操作!
-
不要滥用Optional,比如在java bean的getter中!