可选项 vs 抛出异常

24

自Java 1.8以来,返回Optional对象是否比抛出异常更可取?越来越多的代码看起来像这样:

  public Optional<?> get(int i) {
        // do somtething
        Object result = ...
        Optional.ofNullable(result);
    }

不要这样:

public Object get(int i) {
        if(i<0 || i>=size) {
            throw new IndexOutOfBoundsException("Index: " + i + ". Size: " + size);
        }
        // do somtething
        Object result = ...
        return result;
    }

这是否意味着我们需要忘记旧方法并使用新方法?而Optional在什么情况下是适用的呢?


2
我认为在大多数情况下,Optional将替换return null;而不是抛出异常。但在两种情况下都取决于具体情况。使用Optional而不是null的想法是让缺失更加明确,并减少NullPointerExceptions的风险。 - eckes
3个回答

40
你所提供的示例并不是Optional的适当使用方式。空的Optional表示一个值因为调用者无法预测的原因而不存在。它是方法合法调用的结果。
你提供的“旧惯例”代码对输入进行验证,如果输入无效,则抛出未检查的异常。即使引入Optional,这种行为也应该保持不变。唯一的区别是Object get(i)的返回值可能为空,而Optional<?> get(i)的返回值永远不会为空,因为Optional实例存在一种特殊状态,表示缺少值。
使用返回Optional而不是可空值的方法的优点在于消除了必须在尝试处理返回值之前进行例行的null检查的样板代码。仅在方法内部纯粹使用Optional还有许多其他优点。例如:
static Optional<Type> componentType(Type type) {
  return Optional.of(type)
                 .filter(t -> t instanceof ParameterizedType)
                 .map(t -> (ParameterizedType) t)
                 .filter(t -> t.getActualTypeArguments().length == 1)
                 .filter(t -> Optional.of(t.getRawType())
                                      .filter(rt -> rt instanceof Class)
                                      .map(rt -> (Class<?>) rt)
                                      .filter(Stream.class::isAssignableFrom)
                                      .isPresent())
                 .map(t -> t.getActualTypeArguments()[0]);

这里最重要的好处是完美的作用域控制:在每个新作用域中,相同名称t被重新使用为适合处理阶段的变量类型。因此,与其被迫使变量在其有用寿命过期后仍然在作用域内,并为每个后续变量发明新名称,使用这种习惯用法可以确切地获得我们需要继续进行的最小限度。

只是为了增加趣味性,您可以完全基于Optional来实现equals

@Override public boolean equals(Object obj) {
  return Optional.ofNullable(obj)
                 .filter(that -> that instanceof Test)
                 .map(that -> (Test)that)
                 .filter(that -> Objects.equals(this.s1, that.s1))
                 .filter(that -> Objects.equals(this.s2, that.s2))
                 .isPresent();
}

虽然我认为这个习语非常简洁易读,但目前还不够优化,不能作为一个值得推荐的生产级选择。未来版本的Java可能会使其可行。


4
尽管我同意你的推理,但我想知道在你所呈现的这段代码中,需要练习多少次才能让我像非函数式风格的代码一样容易读懂。老实说,在这种情况下,我认为传统的代码在可读性方面更胜一筹。但是再说一遍,我从未接触过太多的 Haskell。 - Michael Piefel
3
我从未写过Haskell或任何类似的语言,但是在认真使用几周后,Optional习惯用法变得很自然。当然,这只是个人情况,但我的预测是大多数人不需要超过一个月或两个月就可以适应并开始受益。 - Marko Topolnik
我非常喜欢使用Optional来避免空指针异常,但在这个非常短的“流”上使用map甚至更多的是filter看起来有点奇怪。 - Michael Piefel
3
是的,你需要调整自己对于正在发生的事情的心理画面。美妙之处在于,没有Optional,你要么需要用另一个if嵌套层级或者早期返回来替换每个“filter”。对于上述代码中的内部Optional,如果你想使用早期返回习惯用法,你可能需要一个单独的方法。与我的起点相比,这段代码需要花费相当多的注意力和时间才能跟踪每个标识符名称到其声明,并追踪每个名称的使用情况。 - Marko Topolnik
@MarkoTopolnik,值得注意的是,使用Optional.get有点违背了Optional的初衷,因为它可能会抛出NoSuchElementException异常,而且忘记isPresent()检查和忘记!= null检查一样容易。 - Andy
显示剩余2条评论

31

滥用异常、空指针和Optional都是可能的。在这种情况下,我认为你可能滥用了Optional,因为你会默默地隐藏前置条件违规并将其转换为正常返回。从你的代码接收到一个空Optional时,调用者无法区分“我正在寻找的东西不在那里”和“我问了一个无效的问题”。

由于Optional很新,人们也倾向于过度使用它;希望随着时间的推移,正确的模式将被内化。

Optional是“null对象模式”的一个例子;当“没有东西”是合理的预期结果时,它提供了一种安全的方式来表示“没有东西”。(返回空数组或空集合在这些领域中是类似的示例。)是否应该通过null/optional而不是异常来表示“没有东西”通常取决于“没有东西”是否是经常预期的情况,还是异常情况。例如,如果映射不存在,则没有人希望Map.get抛出异常;映射不存在是预期的而不是异常的结果。(如果我们在1997年有Optional,Map.get可能已经返回Optional。)

我不知道你从哪里听到Optional优于异常的建议,但告诉你这个的人是不明智的。如果你以前抛出异常,你可能仍应该抛出异常;如果你以前返回null,你可以考虑返回Optional。


2
顺便问一下,目前是否有关于 Optional 的积极优化工作正在进行中?例如短路链接的 filter 调用?我发现无条件调用是导致使用 Optional 实现的 equals 性能下降的关键因素。 - Marko Topolnik
2
@MarkoTopolnik 目前还没有;我们正在将优化工作投入到更大的目标中。 - Brian Goetz
在这些领域中,返回一个空数组或空集合是类似的例子。那么为什么JDK8没有像Guava Optional.asSet这样方便的Optional-to-Collection桥接呢?虽然有一种笨拙的解决方法,但我认为现在还不算太晚将其添加到Java中。 - Tomáš Záluský
3
“显而易见”的方法集合,被要求添加到Optional中的数量似乎是无限的。” - Brian Goetz
@BrianGoetz infinite - 为什么要使用如此极端的表达方式?结合Guava和JDK的功能就足够了。在处理返回null函数的Optional.transform方面,我在Guava方面也遇到了类似的不愿意,相反,我认为JDK的方法更加灵活。 - Tomáš Záluský
@BrianGoetz:“例如,没有人希望Map.get在映射不存在时抛出异常。”这是一个不正确的说法。问题在于许多Java类的设计是不一致的。当键不存在时,Map<K,V>.get()返回null,但是List<T>.get()基本上相似的情况下会抛出异常。因此,无论是否有人期望异常,都取决于情况/场景。如果这样的类有两个API:get()find(),一个抛出异常,另一个返回null(或更好的是Optional),那么就会更好。 - Nawaz

5
在可能出现错误的情况下,适合使用的数据类型是Try。
与使用“存在”或“空”这些抽象不同,Try使用“失败”或“成功”这些抽象。
由于Java 8没有提供Try,因此需要使用一些第三方库。(也许我们会在Java 9中看到它的添加?)
Java的Try:https://github.com/lambdista/try

or Either/Left/Right - Jarek Przygódzki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接