Java|使用 Optional 更好地处理 null 返回值

目录

Optional 是一个容器类,是 JDK 8 提供的一个防止引起空指针异常的工具类,可以更好地封装处理返回值。

为什么更推荐使用 Optional 类封装可能为 null 的返回值?在项目开发过程中,没人绝对清楚调用方法的返回值一定存在,开发者也只是尽力保证返回值不为 null,比如查询用户列表没用户时我们就返回长度为 0 的 ArrayList,有种对任何值都不信任的编程方式,但这种方式会形成许多冗余代码,让开发者也很累,不这样做会带来讨厌的 Null Pointer Exception(NPE)问题;其次是在多层次取成员变量时,程序员能判断到吐。Optional 的出现很好的改观了这个问题,一两行代码就能代替繁琐的判空。

本文对 Optional 工具的使用技巧进行总结。

热身

开始前你可以先熟悉一下 Optional 的 API,前往菜鸟教程

坏例子

例子类:

class User {
  Long id;
  String name;
  Address addr;
}

一个查询 User 的方法:

  // 查询方法
  User findUserById(Long id);

坏例子 1:

  void method1(Long id) {
    User u = dao.findUserById(id);

    // 此时开发人员可能使用 User 类对象 u 疏忽了判空
    u.getName(); // 用户未查到,u 为 null,发生 NPE
  }

坏例子 2:

  void method2(Long id) {
    User u = dao.findUserById(id);

    // 现在要取成员变量 Address 的城市。
    String city = null;
    if (u != null ) { // 判空
      String name = u.getName();
      Address addr = u.getAddress();
      // 这里还要判空,如果不判空可能发生 NPE
      if (addr != null) {
        prov = addr.getCity();
      }
    }
    // 使用
    System.out.println(city);
  }

相信看完这个例子你已经厌恶空指针错误了,在 JDK 1.8 之前,我们通常没有好的解决办法。

这种方式首先在 Guava 上出现,JDK 1.8 吸取了这个良好的设计,接下来我们看下使用 Optional 怎么样避免上面的问题。

改良后的查询方法:

  // 查询方法
  Optional<User> findUserById(Long id) {
    if (id == null) {
      return Optional.empty();
    }

    User user = dbUtils.query(id);
    return Optional.ofNullable(user);
  }

对例子 1 NPE 调用改造:

  void method1(Long id) {
    Optional<User> u = dao.findUserById(id);
    // before
    // u.getName(); // 用户未查到,u 为 null,发生 NPE

    // after
    // 我们拿到了 name,它可能存在,也或者为已经猜测到的 null,中间不存在任何发生 NPE 的风险,
    String name = u.map(User::getName).orElse(null);
  }

当我们刻意使用 Optional 包装返回值时,同时也是潜意识的提醒用户:现在返回的这个值,可能为 null。

对例子 2 调用改造:

  void method2(Long id) {
    Optional<User> u = dao.findUserById(id);

    // before
    // if (u != null ) { // 判空
    //   String name = u.getName();
    //   Address addr = u.getAddress();
    //   // 这里还要判空,如果不判空可能发生 NPE
    //   if (addr != null) {
    //     prov = addr.getCity();
    //   }
    // }

    // after
    // 现在要取成员变量 Address 的城市。
    String city = u.map(User::getAddr).map(Address::getCity).orElse("CITY EMPTY");

    // 使用非常明确的对象
    System.out.println(city);
  }

Optional 只用一行链式调用就帮我们结束了多层嵌套的判空。

到这里相信你已经理解 Optional 的作用了,它最常用的方式就是上面的例子。

从源码看使用

1、注意点: Optional.of(obj) 只能接收非空 obj 对象,若要传入可能为 null 的对象就使用 Optional.ofNullable(obj)

  // 源码

  // of 方法
  public static <T> Optional<T> of(T value) {
      return new Optional<>(value);
  }

  // of方法用的构造器
  private Optional(T value) {
    // 判空,value 为 null 抛异常
    this.value = Objects.requireNonNull(value);
  }

2、注意点: get() 不能直接使用。

  public T get() {
      // 判空抛异常!
      if (value == null) {
          throw new NoSuchElementException("No value present");
      }
      return value;
  }

3、flatMapmap 的区别: flatMap必须传入 Optional 对象,而 map 不用。

使用对比:

  Optional<User> u = dao.findUserById(id);
  // 获取 addr 的 city

  // flatMap
  String city = u.flatMap(user -> Optional.ofNullable(user::getAddr)).flatMap(addr -> Address::getCity).orElse(null);

  // map
  String city = u.map(User::getAddr).map(Address::getCity).orElse(null);

以下为二者的源码。

flatMap

  public<U> Optional<U> flatMap(Function<? super T, Optional<U>/** 这里限定了Optiona对象 **/> mapper) {
      Objects.requireNonNull(mapper);
      if (!isPresent())
          return empty();
      else {
          return Objects.requireNonNull(mapper.apply(value));
      }
  }

map

  public<U> Optional<U> map(Function<? super T, ? extends U/** 这里没有限定 **/> mapper) {
      Objects.requireNonNull(mapper);
      if (!isPresent())
          return empty();
      else {
          return Optional.ofNullable(mapper.apply(value));
      }
  }

错误的使用方式

回到原始:

  Optional<User> u = dao.findUserById(id);

  String name = null;
  if (u.isPresent()) {
    User user = u.get();
    name = user.getName();
  }

  // 使用 name

这种方式多此一举,简单取成员变量没有必要用 Optional。

  User u = dao.findUserById(id);

  Optional<User> userOpt = Optional.ofNullable(u);
  String name = userOpt.map(User::getName).orElse(null);

  // 传统方式一行解决
  String name = u != null ? u.getName() : null;

  // 使用 name

请记住不要使用这些方式让开发变复杂。

总结

小结一下。

1、Optional 通常只使用在返回值可能为 null 的方法作为返回值。

2、Optional 非常适合取一个对象的多层嵌套成员变量。

3、注意 of 的入参不为 null,这是个炸弹。

4、注意使用 get 前先判断是否为空,这也是个炸弹。

参考: