为什么不调用isPresent()而不是iterator.next()的Optional.get()不好?


23

当使用新的Java8流api在集合中查找一个特定元素时,我编写如下代码:

    String theFirstString = myCollection.stream()
            .findFirst()
            .get();

此处IntelliJ警告说,在不首先检查isPresent()的情况下调用get()的情况。

但是,此代码:

    String theFirstString = myCollection.iterator().next();

...没有警告。

两种技术之间是否存在深刻的区别,从而使流传输方法在某种程度上更加“危险”,所以在不首先调用isPresent()的情况下永远不要调用get()至关重要。到处搜寻,我发现一些文章谈论程序员对Optional <>粗心大意,并假定他们可以随时调用get()。

问题是,我想在我的代码有错误的地方尽可能近地引发异常,在这种情况下,这是我在找不到元素时调用get()的地方。我不想写无用的文章,例如:

    Optional<String> firstStream = myCollection.stream()
            .findFirst();
    if (!firstStream.isPresent()) {
        throw new IllegalStateException("There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>");
    }
    String theFirstString = firstStream.get();

...除非我不知道从get()引发异常有某种危险?


阅读了Karl Bielefeldt的回复和Hulk的评论后,我意识到上面的异常代码有点笨拙,这是更好的东西:

    String errorString = "There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>";
    String firstString = myCollection.stream()
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(errorString));

这看起来更有用,并且在许多地方可能很自然。我仍然觉得我将希望能够从列表中选择一个元素而不必处理所有这些问题,但这可能只是我需要习惯于此类构造。


7
如果您使用orElseThrow,则可以将最后一个示例中的异常引发部分写得更加简洁-所有读者都清楚您打算引发异常。
绿巨人

Answers:


12

首先,请考虑检查是复杂的并且可能相对耗时。它需要进行一些静态分析,并且两个调用之间可能有很多代码。

其次,两个构造的惯用用法非常不同。我在Scala中全职编程,使用Options时间比Java多得多。在这里我需要使用一个我不记得单个实例.get()Option。您几乎应该总是Optional从函数中返回,或者正在使用orElse()改为。与抛出异常相比,这些是处理缺失值的更好的方法。

因为.get()在通常的用法中应该很少调用它,所以当IDE看到.get()并查看其使用是否正确时,它才可以开始进行复杂的检查。相反,iterator.next()是迭代器的正确和普遍使用。

换句话说,这不是一个坏问题,而另一个不是,这是一个常见的情况,它是操作的基础,很可能在开发人员测试期间引发异常,而另一个则是很少使用的问题。在开发人员测试期间可能无法充分行使以引发异常的情况。这些都是您希望IDE向您发出警告的情况。


可能是我需要了解有关此Optional业务的更多信息-我通常把java8东西看作是映射对象列表的一种好方法,该对象列表是我们在Web应用程序中经常做的事情。因此,您应该将Optional <>发送到任何地方?这将需要一些时间来习惯,但这是另一篇SE帖子!:)
电视的弗兰克(Frank)

@TV的Frank少了它们应该存在的地方,更多的是,它们使您可以编写用于转换所包含类型的值的函数,并将它们与map或链接flatMapOptional在大多数情况下,都是避免用null支票括起来的好方法。
Morgen

15

当不知道是否包含它的项目时,可以使用Optional类。该警告是为了通知程序员,他们还有一个可能可以正常处理的附加代码路径。这样做的目的是避免引发异常。您提出的用例不是它设计的目的,因此警告与您无关-如果您确实希望引发异常,请继续并禁用它(尽管仅针对此行,而不是全局)-您应该决定是否逐个实例处理不存在的案例,警告会提醒您执行此操作。

可以说,应该为迭代器生成类似的警告。但是,这从来没有做过,现在添加一个可能会在现有项目中引起太多警告,这是个好主意。

还要注意,抛出自己的异常可能比仅默认异常更为有用,因为包括对预期存在的元素的描述,但在以后的调试中却无济于事。


6

我同意这些没有内在的区别,但是,我想指出一个更大的缺陷。

在这两种情况下,您都有一个类型,它提供的方法不能保证能正常工作,因此您应该在此方法之前调用另一个方法。您的IDE /编译器/ etc是否警告您一种情况还是另一种情况仅取决于它是否支持这种情况和/或您是否已配置为支持这种情况。

话虽这么说,这两段代码都是代码味道。这些表达暗示了潜在的设计缺陷并产生了脆弱的代码。

当然,您想快速失败是正确的,但是至少对我来说,解决方案不能只是耸耸肩,将您的手举到空中(或者堆栈上有一个例外)。

您的收藏集目前可能被认为是非空的,但是您真正可以依靠的只是它的类型:Collection<T>-并不能保证您的非空。即使您现在知道它不是空的,但更改的需求可能会导致代码在前面被更改,并且突然您的代码被空集合击中。

现在,您可以自由地回推并告诉其他人您应该获得一个非空列表。您会很高兴发现“错误”很快。但是,您的软件已损坏,需要更改代码。

将此与更实用的方法进行比较:由于您获得了一个集合,并且它可能为空,因此您可以使用Optional#map,#orElse或除#get之外的任何其他方法来强迫自己处理这种情况。

编译器会为您完成尽可能多的工作,从而使您尽可能多地依赖于获取的静态类型协定Collection<T>。当您的代码从不违反此合同时(例如,通过这两个有臭味的调用链中的任何一个),您的代码在更改环境条件时就不会那么脆弱。

在上述情况下,产生空输入集合的更改对您的代码都没有影响。这只是另一个有效输入,因此仍然可以正确处理。

这样做的代价是您必须花时间进行更好的设计,而不是a)冒着脆弱的代码的风险或b)抛出异常而不必要地使事情复杂化。

最后一点:由于并非所有的IDE /编译器都在没有事先检查的情况下警告iterator()。next()或optional.get()调用,因此此类代码通常会在评论中标记。为了它的价值,我不会让这样的代码允许通过审查,而是要求一个干净的解决方案。


我想我可以就代码异味达成共识-我在上面的示例中做了一个简短的帖子。另一个更有效的情况可能是从列表中删除一个强制性元素,这在应用程序IMO中出现并不奇怪。
电视的弗兰克

发现。“从列表中选择具有属性p的元素”-> list.stream()。filter(p).findFirst()..再次,我们被迫问一个问题:“那里不存在什么?” ..每天的面包和黄油。如果是强制性的并且“就在那儿”-为什么首先将它隐藏在其他东西中?
弗兰克

4
因为也许这是从数据库加载的列表。也许列表是在其他功能中收集的统计集合:我们知道我们有对象A,B和C,我想找到B的数量。许多有效的(在我的应用中是常见的)情况。
电视的弗兰克

我不知道您的数据库,但我不能保证每个查询至少有一个结果。统计集合也是如此-谁说它们将始终是非空的?我同意,直截了当的理由应该有这个或那个元素,但是我更喜欢适当的静态类型而不是文档,或者更糟的是从讨论中获得一些见识。
弗兰克(Frank)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.