[Java8]如何正确使用Optional

Optional是Java8提供的为了解决null安全问题的一个API。善用Optional可以使我们代码中很多繁琐、丑陋的设计变得十分优雅。这篇文章是建立在你对Optional的用法有一定了解的基础上的,如果你还不太了解Optional,可以先去看看相关教程,或者查阅Java文档

使用Optional,我们就可以把下面这样的代码进行改写。

public static String getName(User u) {
    if (u == null || u.name == null)
        return "Unknown";
    return u.name;
}

不过,千万不要改写成这副样子。

public static String getName(User u) {
    Optional<User> user = Optional.ofNullable(u);
    if (!user.isPresent())
        return "Unknown";
    return user.get().name;
}

这样改写非但不简洁,而且其操作还是和第一段代码一样。无非就是用isPresent方法来替代u==null。这样的改写并不是Optional正确的用法,我们再来改写一次。

public static String getName(User u) {
    return Optional.ofNullable(u)
                    .map(user->user.name)
                    .orElse("Unknown");
}

这样才是正确使用Optional的姿势。那么按照这种思路,我们可以安心的进行链式调用,而不是一层层判断了。看一段代码:

public static String getChampionName(Competition comp) throws IllegalArgumentException {
    if (comp != null) {
        CompResult result = comp.getResult();
        if (result != null) {
            User champion = result.getChampion();
            if (champion != null) {
                return champion.getName();
            }
        }
    }
    throw new IllegalArgumentException("The value of param comp isn't available.");
}

由于种种原因(比如:比赛还没有产生冠军、方法的非正常调用、某个方法的实现里埋藏的大礼包等等),我们并不能开心的一路comp.getResult().getChampion().getName()到底。而其他语言比如kotlin,就提供了在语法层面的操作符加持:comp?.getResult()?.getChampion()?.getName()。所以讲道理在Java里我们怎么办!

Java用户听了都想打人

让我们看看经过Optional加持过后,这些代码会变成什么样子。

public static String getChampionName(Competition comp) throws IllegalArgumentException {
    return Optional.ofNullable(comp)
            .map(Competition::getResult)  // 相当于c -> c.getResult(),下同
            .map(CompResult::getChampion)
            .map(User::getName)
            .orElseThrow(()->new IllegalArgumentException("The value of param comp isn't available."));
}

这就很舒服了。Optional给了我们一个真正优雅的Java风格的方法来解决null安全问题。虽然没有直接提供一个操作符写起来短,但是代码看起来依然很爽很舒服。更何况?.这样的语法好不好看还见仁见智呢。

还有很多不错的使用姿势,比如字符串为空则不打印可以这么写:

string.ifPresent(System.out::println);

Optional的魅力还不止于此,Optional还有一些神奇的用法,比如Optional可以用来检验参数的合法性。

public void setName(String name) throws IllegalArgumentException {
    this.name = Optional.ofNullable(name)
                        .filter(User::isNameValid)
                        .orElseThrow(()->new IllegalArgumentException("Invalid username."));
}

这样写参数合法性检测,应该足够优雅了吧。

2019-10-13 补充Optional的本质,提出若干应用建议。

不过这还没完,上面的两个例子其实还不能完全反应出Optional的设计意图。事实上,我们应该更进一步,减少Optional.ofNullable的使用。为什么呢?因为Optional是被设计成用来代替null以表示不确定性的,换句话说,只要一段代码可能产生null,那它就可以返回Optional。而我们选择用Optional代替null的原因,是Optional提供了一个把若干依赖前一步结果的处理结合在一起的途径。举个例子,在我们调用一个网站的登录接口的时候,大概会有以下的步骤:

  1. 发送HTTP请求,得到返回。
  2. (依赖:接口的返回)解析返回,如将Json文本形式的返回结果转化为对象形式。
  3. (依赖:解析的结果)判断结果是否成功。
  4. (依赖:若成功调用的结果)取得鉴权令牌。
  5. (依赖:获得的令牌)进行处理。

其中,第2-5步的每一个步骤都依赖于前一个步骤,而前一个步骤传递过来的数据都具不确定性(有可能是null)。所以,我们可以把它们接受的数据都设计成Optional。第1-4步每一个步骤的结果也具备不确定性,所以我们也把它们的结果设计成Optional。最后到了第5步,我们终于要对一切的结果进行处理了:如果成功获得令牌就存储,失败就提示用户。所以这一步,我们采用如orElse之类的方法来消除不确定性。于是我们最后的设计就可以是:

  1. 结果String(可能是null) == 包装 ==>  Optional<String>
  2. Optional<String> == 解析 ==>  Optional<Json对象>
  3. Optional<Json对象>  == Filter判断成功 ==>  Optional<Json对象>
  4. Optional<Json对象>  == 取鉴权令牌 ==>  Optional<AuthToken>
  5. Optional<AuthToken>进行处理,消除Optional

Optional就像一个处理不确定性的管道,我们在一头丢进一个可能是null的东西(接口返回结果),经过层层处理,最后消除不确定性。Optional在过程中保留了不确定性,从而把对null的处理移到了若干次操作的最后,以减少出现NPE错误的可能。于是,Optional应用的建议也呼之欲出了:

  1. 适用于层级处理(依赖上一步操作)的场合。
  2. 产生对象的方法若可能返回null,可以用Optional包装。
  3. 尽可能延后处理null的时机,在过程中使用Optional保留不确定性。
  4. 尽量避免使用Optional作为字段类型。

最后说句题外话,这种依赖上一步的操作也叫Continuation。而Optional的这种接受并组合多个Continuation的设计风格就是Continuation-passing style(CPS)。

参考资料

使用 Java8 Optional 的正确姿势 – 隔叶黄莺 Unmi Blog (https://unmi.cc/proper-ways-of-using-java8-optional/)

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片

评论

  1. 开发者头条 2017.07.31 9:30上午

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/vb4vdi 欢迎点赞支持!
    欢迎订阅《KAAAsS的J&A教室》https://toutiao.io/subjects/69380

  2. Yanbin 2017.08.31 11:07上午

    不错,我还是喜欢未备案的网站
    请问你的源代码显示是用的什么插件?

    • KAAAsS 2017.09.02 6:40下午

      Crayon Syntax Highlighter

  3. Yanbin 2017.09.21 11:02上午

    我也改用成了这个源代码高亮插件。
    自从 Java 8 有了 Optional 之后,又很容易被过度使用,一个典型就是方法中一上来就是
    Optional.ofNullable(u),本来这个参数是不允许为 null 的,这样反而隐藏了错误。所以如果参数确定不允许为 null, 就应该作一个参数断言,抛出 IllegalArgumentException.

    • d 2019.09.06 5:52下午

      Optional.of(xxx);
      这样如果xxx是null,会抛错

      • KAAAsS 2019.09.06 6:02下午

        Optional.of本来就不接受null。层主的意思是,就算Optional.ofNullable可以接受null,也应该尽可能避免。

  4. zb 2018.06.21 5:29下午

    public static String getName(User u) {
    return Optional.ofNullable(u)
    .map(user->user.name)
    .orElse("Unknown");
    }
    例1这样改当name为null时候返回的结果跟改之前并不是一样的吧

    • KAAAsS 2018.06.22 3:36下午

      应该是一样的。
      是的……如果user为null的话相同,user.name为null就不同了。我这里带上了user不为null则user.name不为null的前提,导致原先的代码不是很健壮……

在此评论中不能使用 HTML 标签。