优雅处理null,向空指针说再见!

导语

        作为一位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中!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mandy_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值