乐趣区

【修炼内功】[Java8] 使用Optional的正确姿势及序列化问题

本文已收录【修炼内功】跃迁之路

Java8 的 Optional 为解决 '空' 的问题带来了很多新思路,查看 Optional 源码,实现非常简单,逻辑也并不复杂。Stuart Marks在其一次演讲中花了约 1 个小时的时间来讲述如何正确的使用 Optional (Optional – The Mother of All Bikesheds by Stuart Marks),也有人调侃道 1 hour for Optional, you gotta be kidding me. 使用 Optional 不难,但用好 Optional 并不容易

Stuart Marks 在演讲中提到了 Optional 的基本作用

Optional is intended to provide a limited mechanism for library method return types where there is a clear need to represent “no result”, and where using null for that is overwhelmingly likely to cause errors.

在以往的编程模型中,对于“没有内容”,大多数情况需要使用 null 来表示,而 null 值总是被人忽略处理(判断),从而在使用过程中极易引起 NPE 异常

Optional的出现并不是为了替代 null,而是用来表示一个不可变的容器,它可以包含一个非 null 的T 引用,也可以什么都不包含(不包含不等于 null),非空的包含被称作persent,而空则被称作absent

本质上讲 Optional 类似于异常检查,它迫使 API 用户去关注 / 处理 Optional 中是否包含内容,从而避免因为忽略 null 值检查而导致的一些潜在隐患

假设有一个函数用来根据 ID 查询学生信息public Student search(Long id),现在有一个需求,需要根据 ID 查询学生姓名

public String searchName(Long id) {Student student = search(id);
    return student.getName();}

注意,search 函数是可能返回 null 的,在这种情况下 searchName 很有可能会抛出 NPE 异常

public String searchName(Long id) {Student student = search(id);
    return Objects.nonNull(student) ? student.getName() : "UNKNOWN";}

除非特别明确函数的返回值不可能为 null,否则一定要做 null 值检查,虽然这样写并没有增加太大的编码负担,但人总归是懒惰的,忽略检查的情况也总是会出现

如果我们改造 search 函数返回 Optional,public Optional<Student> search(Long id),再来重写 searchName 函数

public String searchName(Long id) {Optional<Student> student = search(id);
    return student.getName();}

这样的代码是编译不过的,它会强制让你去检查 search 返回的值是否有内容

public String searchName(Long id) {Optional<Student> student = search(id);
    return student.map(Student::getName).orElse("UNKNOWN");
}

Optional 的使用可以参考其 API 文档,以下内容假设您已了解如何使用 Optional

但是否就应该消灭 null,全部使用 Optional 来替代,回答当然是 NO,null 自有它的用武之地,Optional 也并不是全能的

kotlin 等语言,使用 ?. 符号来解决 java 中 if...else…... ? ... : ...的啰嗦写法,如上问题可以使用student?.name : null,其语义为 ” 当 studen 不为 null 时取其 name 属性值,否则取 null 值 ”,kotlin 的语法只是简化了编程方式,让编程变得更 ” 爽 ”,但并没有解决 ” 人们容易忽略 null 值检查 ” 的情况

Stuart Marks 从 5 个不同的角度详细讲述了如何使用 Optional,这里不一一叙述,有兴趣的可以直接跳到视频去看,下面将从 Stuart Marks 提到的 7 个 Optional 使用规范,来讲述如何正确使用 / 不要滥用 Optional,最后重点解释一下【为什么 Optional 不能序列化

0x00 使用规约

Rule 1: Never, ever, user null for an Optional variable or return value.

Optional 也是一个引用类型(reference type),其本身也可以赋值为 null,如果我们在使用 Optional 的时候还要多做一层 null 检查,就违背了 Optional 的设计初衷,所以在任何时候都不要将 Optional 类型的变量或返回值赋值为 null,我们希望的是在遇到 Optional 的时候不需要关心其是否为 null,只需要判断其是否有值即可

public String searchName(Long id) {Optional<Student> student = search(id);
    if (Objects.isNull(student)) {
      // Optional 可能为 null,这严重违背了 Optional 的设计初衷
      return null;
    }
    return student.map(Student::getName).orElse("UNKNOWN");
}

Rule 2: Never user Optional.get() unless you can prove that the Optional is present.

如果 Optional 是 absent(不包含任何值)的时候使用 Optional.get(),会抛出 NoSuchElementException 异常,该接口的设计者也承认其设计的不合理,之后的某个 jdk 版本中可能会将其标记为@Deprecated,但还没有计划将其移除

public String searchName(Long id) {Optional<Student> student = search(id);
    // 直接使用 get(),可能会抛 NoSuchElementException 异常
    return student.get().getName();
}

如果确实需要在 Optional 无内容的时候抛出异常,也请不要使用 Optional.get()方法,而要使用 Optional.getOrThrow()方法,主动指定需要抛出的异常,虽然该方法并未在 jdk8 中设计,但已经有计划在接下来的 jdk 版本中加入

Rule 3: Prefer alternatives to Optional.isPresent() and Optional.get().

如果一定要使用 Optional.get(),请一定要配合 isPresent(),先判断 Optional 中是否有值

public String searchName(Long id) {Optional<Student> student = search(id);
    // 如果一定要使用 Optional.get(),请一定要配合 isPresent()
    return student.isPresent() ? student.get().getName() : "UNKNOWN";}

Rule 4: It’s generally a bad idea to create an Optional for the specific purpose of chaining methods from it to get a value.

链式语法可以让代码的处理流程看起来更加清晰,但是为了链式而去使用 Optional 的话,在某些情况下并不会显得有多优雅

比如,本来可以使用三目运算

String process(String s) {return Objects.nonNull(s) ? s : "DEFAULT";
}

如果非要硬生生地使用链式的话

String process(String s) {return Optional.ofNullable(s).orElse("DEFAULT");
}

比如,本来可以使用 if 判断值的有效性

BigDecimal first = getFirstValue();
BigDecimal second = getSecondeValue();

if (Objects.nonNull(first) && Objects.nonNull(second)) {return first.add(second.get());
} else {return Objects.isNull(first) ? second : first;
}

如果非要使用链式

Optional<BigDecimal> first = getFirstValue();
Optional<BigDecimal> second = getSecondeValue();
return Stream.of(first, second)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .reduce(BigDecimal::add);

或者

Optional<BigDecimal> first = getFirstValue();
Optional<BigDecimal> second = getSecondeValue();
return first.map(b -> second.map(b::add).orElse(b))
            .map(Optional::of)
            .orElse(second);

从可读性及可维护性上来讲并没有提升,反而会带来一丝阅读困难,并且上文说过,Optional 本身为引用类型,创建的 Optional 会进入堆内存,如果大量的不合理的使用 Optional,也会在一定程度上影响 JVM 的堆内存及内存回收

Rule 5: If an Optional chain has a nested Optional chain, or has an intermediate result of Optional<Optional<T>>, it’s probably too complex.

在使用 Optional 的时候,一定要保证 Optional 的简洁性,即 Optional 运算过程中所包含的类型既是最终需要的类型值,不要出现 Optional 嵌套的情况

Optional<BigDecimal> first = getFirstValue();
Optional<BigDecimal> second = getSecondeValue();

if (!first.isPresent && ! sencond.isPresent()) {return Optional.empty();
} else {return Optional.of(first.orElse(ZERO).add(second.orElse(ZERO)));
}

这样的写法,会对代码的阅读带来很大的困扰

Rule 6: Avoid using Optional in fields, method parameters, and collections.

尽量避免将 Optional 用于类属性、方法参数及集合元素中,因为以上三种情况,完全可以使用 null 值来代替 Optional,没有必要必须使用 Optional,另外 Optional 本身为引用类型,大量使用 Optional 会出现类似 (这样描述不完全准确) 封箱、拆箱的操作,在一定程度上会影响 JVM 的堆内存及内存回收

Rule 7: Avoid using identity-sensitive operations on Optionals.

首先需要解释,什么是identity-sensitive,可以参考 object identity and equality in java

identity-sensitive operations 包含以下三种操作

  • reference equality,也就是 ==
  • identity hash code
  • synchronization

Optional 的 JavaDoc 中有这样一段描述

This is a value-based class; use of identity-sensitive operations (including reference equality(==), identity hash code, or synchronization) on instances of Optional may hava unpredictable results and should be avoided.

总结下来,就是要避免使用 Optional 的 == equals hashCode 方法

在继续之前,需要再解释一下什么是 value type

value type – Project Valhalla

  • an “object” that has no notion of identity
  • “code like a class, works like an int”
  • we eventually want to convert Optional into a value type

vale type,首先像一个类(至少从编码角度来讲),但是却没有类中 identity 的概念,运行的时候却又和基本类型很相似

简单来说就是编码的时候像类,运行的时候像基本类型

显然,Optional 目前还不是 value type,而是reference type,我们查看 Optional 类的equalshashCode方法,并没有发现有什么特别之处,但是有计划在接下来的某个 jdk 版本中将 Optional 定义为value type

@Override
public boolean equals(Object obj) {if (this == obj) {return true;}

    if (!(obj instanceof Optional)) {return false;}

    Optional<?> other = (Optional<?>) obj;
    return Objects.equals(value, other.value);
}

@Override
public int hashCode() {return Objects.hashCode(value);
}

在 jdk8 中使用 Optional 的 identity-sensitive operations 其实并没有太大问题,但很难保证,在今后的后一个 jdk 版本中将 Optional 定义为 value type 时不会出问题,所以为了兼容 jdk 升级程序逻辑的正确性,请避免使用 Optional 的identity-sensitive operations

这也引出了 Optional 为什么不能序列化

0x01 序列化问题

首先,需要了解 jdk 中序列化的一些背景

  • JDK rule: forward and backword serialization compatibility across releases
  • If Optional were serializable today, it would be serialized as an Object

    • it’all always be serialized as an Object, even if eventually becomes a value type
  • Serialization inherently depends on object identity
  • Consequencds of Optional being serializable

    • it might prevent it from being converted into a value type in the future
    • deserializing and Optional might result in a “boxed” value type
  • 首先,JDK 的序列化比较特殊,需要同时 向前 向后 兼容,如在 JDK7 中序列化的对象需要能够在 JDK8 中反序列化,同样在 JDK8 中序列化的对象需要能够在 JDK7 中能够反序列化
  • 其次,序列化需要依赖于对象的identity

有了以上两个序列化的前提条件,我们再来看 Optional,上面已将说过,虽然目前 Optional 是 reference type 的,但其被标记为value based class,有计划在今后的某一个 JDK 版本中将其实现为value type

如果 Optional 可以序列化,那现在就有两个矛盾点

  • 如果 Optional 可以序列化,那接下来的计划中,就没办法将 Optional 实现为value type,而必须是reference type
  • 或者将 value type 加入identity-sensitive operations,这对于目前所有已发行的 JDK 版本都是相冲突的

所以,虽然现在 Optional 是 reference type,但有计划将其实现为value type,考虑到 JDK 序列化的 向前 向后 兼容性,从一开始就将 Optional 定为不可序列化,应该是最合适的方案了

如果真的有在类属性上使用 Optional 的需求怎么办?这里有两个替代方案 / 讨论可以参考

  • Optional Pragmatic Approach
  • Nothing is better than the Optional type
退出移动版