对于 Optional 的解释

SpongeAPI 的很多部分都使用了 Java 提供了 Optional 系统作为对象的获取方式,然而如果你之前从未使用过 Optional 的话,你会觉得这种用法有一点点奇怪。你也许会问:“为什么我需要使用这么一套额外的步骤去获取 API 里的一些对象?”

这一节简要概括了 Optional 的重要性,同时说明了如何——以及或许更重要的是为什么——整个 SpongeAPI 都在使用它。

我们从一点点的历史开始,并且看一看一个访问器——特别是 Getter ——在通常情况下没有 Optional 时是如何工作的。

一、隐式的 Null 可能性以及它为什么那么糟糕

我们假设有一个简单的 API 对象 Entity 并有着一个返回一个 FoogetFoo() 方法。

../../_images/optionals1.png

在比较早的年代,我们的插件可能是这样通过 Getter 获取和使用这个 EntityFoo 的:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

问题随之出现——因为当设计 API 的时候——当考虑到 getFoo 到底会不会返回一个 null ,我们就不得不去依赖一个 隐式 约定,这样的 隐式约定 可以有这样两种方式:

  • 在 JavaDoc 中——这很糟糕,因为它依赖于插件作者怎么去阅读它,因此插件作者并不能够很清楚这个约定到底是什么样子

  • 使用 Nullable 注解——这也不是很理想,因为在一般情况下这些注解需要一些工具才能真正起到作用,比如依赖于 IDE 或者编译器。

../../_images/optionals2.png

我们现在假设这么一个 getFoo() 方法可能——作为一个约定——返回一个 null。这意味着我们上面的代码是不安全的因为它会导致在返回一个 null 的时候直接抛出一个空指针异常(NullPointerException)。

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

我们现在假设我们的插件作者很了解这个 getFoo 方法返回 null 会发生什么,所以他决定通过检查 null 的方式修复这么一个问题。假设这里定义了一个本地常量 Foo 那么最后的代码看起来就会是这个样子:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

在这个示例中,插件作者意识到了这个方法可能返回 null ,然后定义了一个常量用于 Foo 的默认实现以替代它。当然这么一个插件可以完全不需要这么一个方法调用,也可以通过别的地方获取这么一个 Foo。不过这里的重点是处理 null 会很容易导致代码变得一团糟,即使只是简单的情况,而且还取决于插件的作者是否直接去了解了这一方法的约定以确定到底要不要首先检查一下返回值是不是 null。

然而,这并不是唯一的缺点。现在让我们从长远考虑这一 API,然后假设作者在写插件的时候,它们浏览了方法的 JavaDoc,然后看到了这一方法被保证永远不会返回 null (因为每个 Entity 总会有一个 Foo 可用)。太好了!根本不需要任何的 null 检查!

然而,现在我们假设,在以后版本的游戏中,游戏开发者把 Foo 删除或弃用(Deprecate)了。然后这一 API 的作者就相应地更新了它的 API,并声称目前 getFoo() 方法 返回一个 null,然后写进了 JavaDoc。那么问题来了:即便是勤奋的插件作者在之前查阅了相关的 JavaDoc,然后在他们首次撰写代码的时候没有检查 null,那么目前它就不知不觉得产生了问题:没有在适当的位置检查 null 的使用了 Foo 的代码都会产生一个空指针异常。

因为我们看到,如果我们允许可能的 null,那么我们就要在一系列相当糟糕的解决方案中选择:

  • 插件作者可以假设所有的方法都可能返回一个 null 并在相应的地方写代码防守,然后我们已经看到了这会导致的一团糟的代码。

  • API 作者可以在每一个 API 方法上定义一个隐式的 null 约定,以试图处理 null 的方式解决插件作者的问题,但这只会加剧前面的问题。

  • API 作者可以声称他们的任何隐式的 null 约定都不会在将来被改变。这意味着如果需要移除一个特性,他们必须要么:

  • 抛出一个异常——几乎不够优雅,不过倒是比到处都是的空指针异常更好追踪一些

  • 返回一个“假”的对象或者一个无效值——这意味着 API 的使用者(插件)将继续工作,但是这为 API 的开发者产生了不断上升的负担,因为每一个弃用的特性都需要创建一个无效的对象。这很快导致一个体积巨大的 API 里充斥着无用的对象,然而这些对象的功能只是为了支持 API 中那些已经弃用的特性。

所以我们已经十分清楚,对于 null 的 隐式 约定会导致一些相当头痛的问题,并会在 API 作为一个极不稳定的底层的中间层时加剧。不过幸运的是,我们有一种更好的方式:

二、Optional 和显式的 null 可能性

如上文所述,用于 Minecraft 的 API 处境十分艰难。最终他们需要提供一个带有 合理数量的隐含稳定 平台凌驾于一个 根本没有隐含稳定 的游戏之上。因此 Minecraft 的任何 API 都需要在设计的时候清楚地意识到,游戏的任何方面都应该对在任何时间以任何理由以任何可以想象的方式被修改负责,适应和包括被完全移除的部分!

这种不稳定性就是导致上面提到的 null 可能性的罪魁祸首。

Optional 通过把 隐式 的 null 可能性转变为 显式 的来解决这一问题。API 从来都不宣称,“ 这是你的对象,好的,谢谢,再见 ”,取而代之的是一个访问器,“ 这是个盒子,里面可能有一个你想要的对象,你看着办 ”。

../../_images/optionals3.png

通过一个可能返回 null 的明确约定,我们把 null 检查 的概念替换成有一点点细微差别的概念 可能存在 。我们也从 一开始 就遵照这一约定。

所以这到底是什么意思?

一言以蔽之,插件作者不再需要担心返回一个 null 的可能性。取而代之的是把一个特定的对象是否存在的可能性包装了起来。这在 null 检查的时候有着与生俱来的特性,同时还带来着更优雅、和可读性更好的代码。

为了看出来为什么会这样,我们来把上面的例子中的 getFoo 方法的返回值设置成一个 Optional<Foo>

public void someEventHandler(Entity someEntity) {
    Optional<Foo> entityFoo = someEntity.getFoo();
    if (entityFoo.isPresent()) {
        entityFoo.get().bar();
    }
}

你可能注意到这一示例看起来和一个标准的 null 检查没有什么区别,然而在同样的代码量下,使用 Optional 的的确确地增加了一些额外的信息。例如,对于想要了解这一方法会不会可能返回 null 的人来说,很明显这一方法可能不会返回我们想要的对象,同时处理这一返回的对象的方式是如此明确和清晰。

那又如何?你呐喊道。我们显式的对 null 可能性的声明和执行对 null 的检查的代码量完全相同——尽管被 Getter 固定住了。“ 那又如何 ”?

好, Optional 这一包装允许我们以比一些传统上的检查方式更优雅的方式,考虑下面的代码:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO);
    entityFoo.bar();
}

停一下!我们就这样把 null 检查和设置默认值这一串繁琐的代码替换成了这么一行?是的,就是这样。实际上,在这个简单的示例里我们甚至可以直接这么做:

public void someEventHandler(Entity someEntity) {
    someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO).bar();
}

这是完全安全的,只要 MyPlugin.DEFAULT_FOO 永远是可用的。

考虑下面的使用两个 Entity 的示例,使用一个隐式的 null 检查我们首先想要得到第一个 EntityFoo,如果第一个 Entity 不存在这个 Foo 我们就从第二个获取,如果都不存在我们就使用默认值:

public void someEventHandler(Entity someEntity, Entity entity2) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = entity2.getFoo();
    }
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

使用 Optional 我们可以让代码变得非常清晰,就像这样:

public void someEventHandler(Entity someEntity, Entity entity2) {
    someEntity.getFoo().orElse(entity2.getFoo().orElse(MyPlugin.DEFAULT_FOO)).bar();
}

这仅仅是 Optional 特性的冰山一角。在 Java 8 中,Optional 还支持 ConsumerSupplier 两个接口,允许 Lambda 表达式在 不存在(Absent) 时使用。一些用法示例可以参阅 用法示例

备注

另一种对于避免 null 的理论依据可以参阅 Guava: Using And Avoiding Null Explained 。请注意,Guava 中的 Optional 类和 Java 的 java.util.Optional 并不一样,因此相关的方法名等也并不一样。