优雅的处理空值

我们在开发过程中经常会遇到返回值为空的问题。有时会抛出NullPointerException,造成很多困扰。今天在importNew读到一篇好文,关于如何优雅的处理null的问题。

 

如有侵权请联系我进行删除

 

作者:Lrwin

传送门

 

导语

 

在笔者几年的开发经验中,经常看到项目中存在到处空值判断的情况,这些判断,会让人觉得摸不这头绪,它的出现很有可能和当前的业务逻辑并没有关系。但它会让你很头疼。

 

有时候,更可怕的是系统因为这些空值的情况,会抛出空指针异常,导致业务系统发生问题。 

 

此篇文章,我总结了几种关于空值的处理手法,希望对读者有帮助。

 

业务中的空值

 

场景

 

存在一个UserSearchService用来提供用户查询的功能:

 

public interface UserSearchService{
  List listUser();

  User get(Integer id);
}

 

问题现场

 

对于面向对象语言来讲,抽象层级特别的重要。尤其是对接口的抽象,它在设计和开发中占很大的比重,我们在开发时希望尽量面向接口编程。

 

对于以上描述的接口方法来看,大概可以推断出可能它包含了以下两个含义:

 

  1. listUser(): 查询用户列表

  2. get(Integer id): 查询单个用户

 

在所有的开发中,XP推崇的TDD模式可以很好的引导我们对接口的定义,所以我们将TDD作为开发代码的”推动者”。

 

对于以上的接口,当我们使用TDD进行测试用例先行时,发现了潜在的问题:

 

  1. listUser() 如果没有数据,那它是返回空集合还是null呢?

  2. get(Integer id) 如果没有这个对象,是抛异常还是返回null呢?

 

深入listUser研究

 

我们先来讨论

 

listUser()

 

这个接口,我经常看到如下实现:

 

public List listUser(){
    List userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){//spring util工具类
      return null;
    }
    return userList;
}

 

这段代码返回是null,从我多年的开发经验来讲,对于集合这样返回值,最好不要返回null,因为如果返回了null,会给调用者带来很多麻烦。你将会把这种调用风险交给调用者来控制。

 

如果调用者是一个谨慎的人,他会进行是否为null的条件判断。如果他并非谨慎,或者他是一个面向接口编程的狂热分子(当然,面向接口编程是正确的方向),他会按照自己的理解去调用接口,而不进行是否为null的条件判断,如果这样的话,是非常危险的,它很有可能出现空指针异常!

 

根据墨菲定律来判断: “很有可能出现的问题,在将来一定会出现!”

 

基于此,我们将它进行优化:

 

public List listUser(){
    List userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){
      return Lists.newArrayList();//guava类库提供的方式
    }
    return userList;
}

 

对于接口(List listUser()),它一定会返回List,即使没有数据,它仍然会返回List(集合中没有任何元素);

 

通过以上的修改,我们成功的避免了有可能发生的空指针异常,这样的写法更安全!

 

深入研究get方法

 

对于接口

 

User get(Integer id)

 

你能看到的现象是,我给出id,它一定会给我返回User.但事实真的很有可能不是这样的。

 

我看到过的实现:

 

public User get(Integer id){
  return userRepository.selectByPrimaryKey(id);//从数据库中通过id直接获取实体对象
}

 

相信很多人也都会这样写。

 

通过代码的时候得知它的返回值很有可能是null! 但我们通过的接口是分辨不出来的!

 

这个是个非常危险的事情。尤其对于调用者来说!

 

我给出的建议是,需要在接口明明时补充文档,比如对于异常的说明,使用注解@exception:

 

public interface UserSearchService{

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体
   * @exception UserNotFoundException
   */
  User get(Integer id);

}

 

我们把接口定义加上了说明之后,调用者会看到,如果调用此接口,很有可能抛出“UserNotFoundException(找不到用户)”这样的异常。

 

这种方式可以在调用者调用接口的时候看到接口的定义,但是,这种方式是”弱提示”的!

 

如果调用者忽略了注释,有可能就对业务系统产生了风险,这个风险有可能导致一个亿!

 

除了以上这种”弱提示”的方式,还有一种方式是,返回值是有可能为空的。那要怎么办呢?

 

我认为我们需要增加一个接口,用来描述这种场景.

 

引入jdk8的Optional,或者使用guava的Optional.看如下定义:

 

public interface UserSearchService{

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体,此实体有可能是缺省值
   */
  Optional getOptional(Integer id);
}

 

Optional有两个含义: 存在 or 缺省。

 

那么通过阅读接口getOptional(),我们可以很快的了解返回值的意图,这个其实是我们想看到的,它去除了二义性。

 

它的实现可以写成:

 

public Optional getOptional(Integer id){
  return Optional.ofNullable(userRepository.selectByPrimaryKey(id));
}

 

深入入参

 

通过上述的所有接口的描述,你能确定入参id一定是必传的吗?我觉得答案应该是:不能确定。除非接口的文档注释上加以说明。

 

那如何约束入参呢?

 

我给大家推荐两种方式:

 

  1. 强制约束

  2. 文档性约束(弱提示)

 

1.强制约束,我们可以通过jsr 303进行严格的约束声明:

 

public interface UserSearchService{
  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体
   * @exception UserNotFoundException
   */
  User get(@NotNull Integer id);

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体,此实体有可能是缺省值
   */
  Optional getOptional(@NotNull Integer id);
}

 

当然,这样写,要配合AOP的操作进行验证,但让spring已经提供了很好的集成方案,在此我就不在赘述了。

 

2.文档性约束

 

在很多时候,我们会遇到遗留代码,对于遗留代码,整体性改造的可能性很小。

 

我们更希望通过阅读接口的实现,来进行接口的说明。

 

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 getOptional(@NonNull Integer id);
}

 

小结

 

通过 空集合返回值,Optional,jsr 303,jsr 305这几种方式,可以让我们的代码可读性更强,出错率更低!

 

  1. 空集合返回值 :如果有集合这样返回值时,除非真的有说服自己的理由,否则,一定要返回空集合,而不是null

  2. Optional: 如果你的代码是jdk8,就引入它!如果不是,则使用Guava的Optional,或者升级jdk版本!它很大程度的能增加了接口的可读性!

  3. jsr 303: 如果新的项目正在开发,不防加上这个试试!一定有一种特别爽的感觉!

  4. jsr 305: 如果老的项目在你的手上,你可以尝试的加上这种文档型注解,有助于你后期的重构,或者新功能增加了,对于老接口的理解!

 

空对象模式

 

场景

 

我们来看一个DTO转化的场景,对象:

 

@Data
static class PersonDTO{
  private String dtoName;
  private String dtoAge;
}

@Data
static class Person{
  private String name;
  private String age;
}

 

需求是将Person对象转化成PersonDTO,然后进行返回。

 

当然对于实际操作来讲,返回如果Person为空,将返回null,但是PersonDTO是不能返回null的(尤其Rest接口返回的这种DTO)。

 

在这里,我们只关注转化操作,看如下代码:

 

@Test
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("");
  }
}

 

优化修改

 

这样的数据转化,我们认识可读性非常差,每个字段的判断,如果是空就设置为空字符串(“”)

 

换一种思维方式进行思考,我们是拿到Person这个类的数据,然后进行赋值操作(setXXX),其实是不关系Person的具体实现是谁的。

 

那我们可以创建一个Person子类:

 

static class NullPerson extends Person{
  @Override
  public String getAge() {
    return "";
  }

  @Override
  public String getName() {
    return "";
  }
}

 

它作为Person的一种特例而存在,如果当Person为空的时候,则返回一些get*的默认行为.

 

所以代码可以修改为:

 

@Test
 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可以进行优化

 

空对象模式,它的弊端在于需要创建一个特例对象,但是如果特例的情况比较多,我们是不是需要创建多个特例对象呢,虽然我们也使用了面向对象的多态特性,但是,业务的复杂性如果真的让我们创建多个特例对象,我们还是要再三考虑一下这种模式,它可能会带来代码的复杂性。

 

对于上述代码,还可以使用Optional进行优化。

 

@Test
  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;
  }

 

Optional对空值的使用,我觉得更为贴切,它只适用于”是否存在”的场景。

 

如果只对控制的存在判断,我建议使用Optional.

 

Optioanl的正确使用

 

Optional如此强大,它表达了计算机最原始的特性(0 or 1),那它如何正确的被使用呢!

 

Optional不要作为参数

 

如果你写了一个public方法,这个方法规定了一些输入参数,这些参数中有一些是可以传入null的,那这时候是否可以使用Optional呢?

 

我给的建议是: 一定不要这样使用!

 

举个例子:

 

public interface UserService{
  List listUser(Optional username);
}

 

这个例子的方法 listUser,可能在告诉我们需要根据username查询所有数据集合,如果username是空,也要返回所有的用户集合.

 

当我们看到这个方法的时候,会觉得有一些歧义:

 

“如果username是absent,是返回空集合吗?还是返回全部的用户数据集合?”

 

Optioanl是一种分支的判断,那我们究竟是关注 Optional还是Optional.get()呢?

 

我给大家的建议是,如果不想要这样的歧义,就不要使用它!

 

如果你真的想表达两个含义,就給它拆分出两个接口:

 

public interface UserService{
  List listUser(String username);
  List listUser();
}

 

我觉得这样的语义更强,并且更能满足 软件设计原则中的 “单一职责”。

 

如果你觉得你的入参真的有必要可能传null,那请使用jsr 303或者jsr 305进行说明和验证!

 

请记住! Optional不能作为入参的参数!

 

Optional作为返回值

 

当个实体的返回

 

那Optioanl可以做为返回值吗?

 

其实它是非常满足是否存在这个语义的。

 

你如说,你要根据id获取用户信息,这个用户有可能存在或者不存在。

 

你可以这样使用:

 

public interface UserService{
  Optional get(Integer id);
}

 

当调用这个方法的时候,调用者很清楚get方法返回的数据,有可能不存在,这样可以做一些更合理的判断,更好的防止空指针的错误!

 

当然,如果业务方真的需要根据id必须查询出User的话,就不要这样使用了,请说明,你要抛出的异常.

 

只有当考虑它返回null是合理的情况下,才进行Optional的返回

 

集合实体的返回

 

不是所有的返回值都可以这样用的!如果你返回的是集合:

 

public interface UserService{
  Optional> listUser();
}

 

这样的返回结果,会让调用者不知所措,是否我判断Optional之后,还用进行isEmpty的判断呢?

 

这样带来的返回值歧义!我认为是没有必要的。

 

我们要约定,对于List这种集合返回值,如果集合真的是null的,请返回空集合(Lists.newArrayList);

 

使用Optional变量

 

Optional userOpt = ...

 

如果有这样的变量userOpt,请记住 :

 

  1. 一定不能直接使用get ,如果这样用,就丧失了Optional本身的含义 ( 比如userOp.get() )

  2. 不要直接使用getOrThrow ,如果你有这样的需求:获取不到就抛异常。那就要考虑,是否是调用的接口设计的是否合理

 

getter中的使用

 

对于一个java bean,所有的属性都有可能返回null,那是否需要改写所有的getter成为Optional类型呢?

 

我给大家的建议是,不要这样滥用Optional.

 

即便 我java bean中的getter是符合Optional的,但是因为java bean 太多了,这样会导致你的代码有50%以上进行Optinal的判断,这样便污染了代码。(我想说,其实你的实体中的字段应该都是由业务含义的,会认真的思考过它存在的价值的,不能因为Optional的存在而滥用)

 

我们应该更关注于业务,而不只是空值的判断。

 

请不要在getter中滥用Optional.

 

小结

 

可以这样总结Optional的使用:

 

  1. 当使用值为空的情况,并非源于错误时,可以使用Optional!

  2. Optional不要用于集合操作!

  3. 不要滥用Optional,比如在java bean的getter中!

 

 

下面是一篇关于Optional类的详解及使用文章。

 

作者:MiZhou

传送门

 

写过 Java 程序的同学,一般都遇到过 NullPointerException :) —— 为了不抛出这个异常,我们便会写如下的代码:

User user = getUserById(id);
if (user != null) {
    String username = user.getUsername();
    System.out.println("Username is: " + username); // 使用 username
}

但是很多时候,我们可能会忘记写 if (user != null) —— 如果在开发阶段就发现那还好,但是如果在开发阶段没有测试到问题,等到上线却出了 NullPointerException ... 画面太美,我不敢继续想下去。


为了解决这种尴尬的处境,JDK 终于在 Java8 的时候加入了 Optional 类。Optional 的 javadoc 介绍:

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

这是一个可以包含或者不包含非 null 值的容器。如果值存在则 isPresent()方法会返回 true,调用 get() 方法会返回该对象。

优雅的处理空值_第1张图片

java.util.Optional

JDK 提供三个静态方法来构造一个 Optional
1.Optional.of(T value),该方法通过一个非 nullvalue 来构造一个 Optional,返回的 Optional 包含了 value 这个值。对于该方法,传入的参数一定不能为 null,否则便会抛出 NullPointerException

2.Optional.ofNullable(T value),该方法和 of 方法的区别在于,传入的参数可以为 null —— 但是前面 javadoc 不是说 Optional 只能包含非 null 值吗?我们可以看看 ofNullable 方法的源码:

Optional.ofNullable 方法

原来该方法会判断传入的参数是否为 null,如果为 null 的话,返回的就是 Optional.empty()

3.Optional.empty(),该方法用来构造一个空的 Optional,即该 Optional 中不包含值 —— 其实底层实现还是 如果 Optional 中的 valuenull 则该 Optional 为不包含值的状态,然后在 API 层面将 Optional 表现的不能包含 null 值,使得 Optional 只存在 包含值不包含值 两种状态。

优雅的处理空值_第2张图片

Optional.empty() 的实现


前面 javadoc 也有提到,OptionalisPresent() 方法用来判断是否包含值,get() 用来获取 Optional 包含的值 —— 值得注意的是,如果值不存在,即在一个Optional.empty 上调用 get() 方法的话,将会抛出 NoSuchElementException 异常。
我们假设 getUserById 已经是个客观存在的不能改变的方法,那么利用 isPresentget 两个方法,我们现在能写出下面的代码:

Optional user = Optional.ofNullable(getUserById(id));
if (user.isPresent()) {
    String username = user.get().getUsername();
    System.out.println("Username is: " + username); // 使用 username
}

好像看着代码是优美了点 —— 但是事实上这与之前判断 null 值的代码没有本质的区别,反而用 Optional 去封装 value,增加了代码量。所以我们来看看 Optional 还提供了哪些方法,让我们更好的(以正确的姿势)使用 Optional

1.ifPresent

Optional.ifPresent

如果 Optional 中有值,则对该值调用 consumer.accept,否则什么也不做。
所以对于上面的例子,我们可以修改为:

Optional user = Optional.ofNullable(getUserById(id));
user.ifPresent(u -> System.out.println("Username is: " + u.getUsername()));

2.orElse

Optional.orElse

如果 Optional 中有值则将其返回,否则返回 orElse 方法传入的参数。

User user = Optional
        .ofNullable(getUserById(id))
        .orElse(new User(0, "Unknown"));
        
System.out.println("Username is: " + user.getUsername());

3.orElseGet

Optional.orElseGet

orElseGetorElse 方法的区别在于,orElseGet 方法传入的参数为一个 Supplier 接口的实现 —— 当 Optional 中有值的时候,返回值;当 Optional 中没有值的时候,返回从该 Supplier 获得的值。

User user = Optional
        .ofNullable(getUserById(id))
        .orElseGet(() -> new User(0, "Unknown"));
        
System.out.println("Username is: " + user.getUsername());

4.orElseThrow

优雅的处理空值_第3张图片

Optional.orElseThrow

orElseThroworElse 方法的区别在于,orElseThrow 方法当 Optional 中有值的时候,返回值;没有值的时候会抛出异常,抛出的异常由传入的 exceptionSupplier 提供。

User user = Optional
        .ofNullable(getUserById(id))
        .orElseThrow(() -> new EntityNotFoundException("id 为 " + id + " 的用户没有找到"));

举一个 orElseThrow 的用途:在 SpringMVC 的控制器中,我们可以配置统一处理各种异常。查询某个实体时,如果数据库中有对应的记录便返回该记录,否则就可以抛出 EntityNotFoundException ,处理 EntityNotFoundException 的方法中我们就给客户端返回Http 状态码 404 和异常对应的信息 —— orElseThrow 完美的适用于这种场景。

@RequestMapping("/{id}")
public User getUser(@PathVariable Integer id) {
    Optional user = userService.getUserById(id);
    return user.orElseThrow(() -> new EntityNotFoundException("id 为 " + id + " 的用户不存在"));
}

@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity handleException(EntityNotFoundException ex) {
    return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}

5.map

优雅的处理空值_第4张图片

Optional.map

如果当前 OptionalOptional.empty,则依旧返回 Optional.empty;否则返回一个新的 Optional,该 Optional 包含的是:函数 mapper 在以 value 作为输入时的输出值。

Optional username = Optional
        .ofNullable(getUserById(id))
        .map(user -> user.getUsername());
        
System.out.println("Username is: " + username.orElse("Unknown"));

而且我们可以多次使用 map 操作:

Optional username = Optional
        .ofNullable(getUserById(id))
        .map(user -> user.getUsername())
        .map(name -> name.toLowerCase())
        .map(name -> name.replace('_', ' '));
        
System.out.println("Username is: " + username.orElse("Unknown"));

6.flatMap

优雅的处理空值_第5张图片

Optional.flatMap

flatMap 方法与 map 方法的区别在于,map 方法参数中的函数 mapper 输出的是值,然后 map 方法会使用 Optional.ofNullable 将其包装为 Optional;而 flatMap 要求参数中的函数 mapper 输出的就是 Optional

Optional username = Optional
        .ofNullable(getUserById(id))
        .flatMap(user -> Optional.of(user.getUsername()))
        .flatMap(name -> Optional.of(name.toLowerCase()));
        
System.out.println("Username is: " + username.orElse("Unknown"));

7.filter

优雅的处理空值_第6张图片

Optional.filter

filter 方法接受一个 Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty

Optional username = Optional
        .ofNullable(getUserById(id))
        .filter(user -> user.getId() < 10)
        .map(user -> user.getUsername());
        
System.out.println("Username is: " + username.orElse("Unknown"));

有了 Optional,我们便可以方便且优雅的在自己的代码中处理 null 值,而不再需要一昧通过容易忘记和麻烦的 if (object != null) 来判断值不为 null。如果你的程序还在使用 Java8 之前的 JDK,可以考虑引入 Google 的 Guava 库 —— 事实上,早在 Java6 的年代,Guava 就提供了 Optional 的实现。


号外:Java9 对 Optional 的增强
即将在今年 7 月到来的 JDK9 中,在 Optional 类中添加了三个新的方法:

  1. public Optional or(Supplier> supplier)
    or 方法的作用是,如果一个 Optional 包含值,则返回自己;否则返回由参数 supplier 获得的 Optional

  2. public void ifPresentOrElse(Consumer action, Runnable emptyAction)
    ifPresentOrElse 方法的用途是,如果一个 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction —— 如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()

  3. public Stream stream()
    stream 方法的作用就是将 Optional 转为一个 Stream,如果该 Optional 中包含值,那么就返回包含这个值的 Stream;否则返回一个空的 StreamStream.empty())。
    举个例子,在 Java8,我们会写下面的代码:

// 此处 getUserById 返回的是 Optional
public List getUsers(Collection userIds) {
       return userIds.stream()
            .map(this::getUserById)     // 获得 Stream>
            .filter(Optional::isPresent)// 去掉不包含值的 Optional
            .map(Optional::get)
            .collect(Collectors.toList());
}

而有了 Optional.stream(),我们就可以将其简化为:

public List getUsers(Collection userIds) {
    return userIds.stream()
            .map(this::getUserById)    // 获得 Stream>
            .flatMap(Optional::stream) // Stream 的 flatMap 方法将多个流合成一个流
            .collect(Collectors.toList());
}

你可能感兴趣的:(优雅的处理空值)