可选项的用途

358

使用Java 8已经有6个月以上了,我对新的API变化感到非常满意。但是我仍然不确定何时应该使用Optional,似乎在任何可能为null的地方都想用它,或者根本不想用。

有很多情况可以使用它,但我不确定它是否增加了好处(可读性/空指针安全)或只会造成额外的开销。

所以,我有几个例子,并且我会很感兴趣听听社区对Optional是否有益的想法。

1 - 当公共方法可能返回null时,作为返回类型:

public Optional<Foo> findFoo(String id);

2 - 当参数可能为 null 时,作为方法参数:

public Foo doSomething(String id, Optional<Bar> barOptional);

3 - 作为bean的可选成员:

public class Book {

  private List<Pages> pages;
  private Optional<Index> index;

}

4 - 在 Collections 中:

总的来说,我认为:

List<Optional<Foo>>

虽然可以使用filter()来删除null值等,但是否有任何在集合中使用Optional的好用途?

我是否遗漏了任何情况?


2
我发现有一个很有用的情况,例如,如果您有一个替换映射。例如 Map<Character, String>。如果没有替换,我可以使用这个:Optional.ofNullable(map.get(c)).orElse(String.valueOf(c))。还要注意的是,Optional是从Guava中偷来的,它有一个更好的语法:Optional.fromNullable(map.get(c)).or(String.valueOf(c)); - fge
3
在集合中,有些集合不允许空值!Optional 可以解决这个问题。你可以使用 .filter(Optional::absent) 将“空值”过滤掉。 - fge
3
公平地说,我认为“Optional”的概念实际上源于函数式编程。 - VH-NZZ
4
@fge是否使用“getOrDefault()”更准确地表达了这个意思? - Jim Garrison
14个回答

275
Optional 的主要设计目标是提供一种方式让函数在没有返回值的情况下表明该值不存在。可以参考这个讨论。这使得调用者可以继续使用链式方法调用。

这最符合OP问题中的使用场景#1。尽管“值不存在”比“null”更精确,因为像IntStream.findFirst之类的东西永远不会返回null。

对于使用场景#2,将可选参数传递给方法是可以实现的,但比较繁琐。假设您有一个接受一个字符串和一个可选第二个字符串的方法。接受Optional作为第二个参数会导致像这样的代码:
foo("bar", Optional.of("baz"));
foo("bar", Optional.empty());

即使接受 null 也更好:

foo("bar", "baz");
foo("bar", null);

最好的方法是拥有一个重载的方法,接受一个字符串参数并为第二个参数提供默认值:

foo("bar", "baz");
foo("bar");

这确实有其局限性,但它比上述两种情况要好得多。

在类字段或数据结构中使用Optional的用例#3#4被认为是API误用。首先,这违反了Optional在顶部所述的主要设计目标。其次,它并没有增加任何价值。

处理Optional中缺失值的三种方法:提供替代值、调用函数提供替代值或抛出异常。如果您要存储到字段中,则应在初始化或分配时间执行此操作。如果要将值添加到列表中,则可以选择不添加该值,从而“展平”不存在的值,正如OP所提到的那样。

我相信某些人可能会想出一些牵强的情况,他们真的想在字段或集合中存储Optional,但通常最好避免这样做。


61
我不同意第三点。当需要根据值的存在或不存在来执行或不执行某些操作时,将Optional作为字段可以很有用。 - glglgl
27
我不明白为什么内部执行if(foo == null)会比只将字段存储为optional更好。此外,我也不明白为什么内部显式调用getOptionalFoo()会比只将字段存储为Optional更好。另外,如果一个字段可以是null,而另一个字段不可以是null,为什么不通过使它们成为Optional或非Optional来告诉编译器呢? - mogronalol
38
@StuartMarks,当我从所有“Optional<>”专家那里听到“不要将Optional<>存储在字段中”时,这是让我感到困惑的事情之一。我真的在努力理解这个建议的意义。考虑一个DOM树,其中一个节点可能有父节点也可能没有。在这种情况下,Node.getParent() 看起来会返回 Optional<Node>。难道我真的应该每次都存储一个 null 并对结果进行包装吗?为什么?除了让我的代码看起来丑陋之外,这额外的低效性还能给我带来什么好处? - Garret Wilson
24
另一方面,假设您有Optional<Node> getParent() { return Optional.ofNullable(parent); }。这看起来很昂贵,因为它会在每次分配一个Optional对象!但是,如果调用者立即解包它,该对象的寿命非常短,并且从未被提升到伊甸园空间之外。它甚至可能会被JIT的逃逸分析消除。最终答案是“视情况而定”,但是在字段中使用Optional可能会膨胀内存使用并减慢数据结构遍历速度。最后,我认为这使代码变得混乱,但这是品味问题。 - Stuart Marks
7
@GarretWilson,所以你写了一篇博客吗?我会很感兴趣的 :) - akhil_mittal
显示剩余12条评论

129

我来晚了,但就我个人而言,我想要补充一下我的看法。这些做法背离了Optional设计目标,Stuart Marks在他的回答中对其进行了很好的总结,但我仍然相信它们的有效性(显然)。

无处不在地使用Optional

总体思路

我写了一篇完整的关于使用Optional的博客文章,但归根结底就是这样:

  • 尽可能避免在类设计中引入可选性
  • 对于所有剩余的情况,默认应该使用Optional而不是null
  • 可能会有例外:
    • 局部变量
    • 私有方法的返回值和参数
    • 性能关键的代码块(不要猜测,使用分析工具)

前两个例外可以降低在Optional中包装和解包引用的开销。它们被选中是为了确保null永远不会合法地从一个实例传递到另一个实例。

请注意,这几乎永远不允许在集合中使用Optional,这几乎和null一样糟糕。别这么做;)

关于您的问题

  1. 是的。
  2. 如果无法进行重载,则是的。
  3. 如果其他方法(子类化、装饰等)不可行,则是的。
  4. 拜托!

优点

这样做可以减少代码库中null的存在,尽管并不能完全消除它们。但这甚至不是主要的优点。还有其他重要的优点:

澄清意图

使用 Optional 明确表示变量是可选的。您的代码读者或 API 使用者将被强烈提示可能不存在值,访问该值前需要进行检查。

消除不确定性

如果没有 Optional,则 null 的含义不明确。它可能是状态的合法表示(参见 Map.get),也可能是实现错误,例如缺少或初始化失败。

这种情况随着 Optional 的持久使用而发生了巨大变化。这里,null 的出现已经表示存在 bug。(因为如果允许值缺失,就会使用 Optional)。这使得调试空指针异常变得更加容易,因为对于这个 null 的含义的问题已经得到了回答。

更多的空值检查

现在,再也不可能出现任何可以为 null 的情况了,这可以在任何地方强制执行。无论是使用注释、断言还是普通检查,您永远不必思考是否可以为 null 的参数或返回类型。都不能!

缺点

当然,没有银弹...

性能

将值(特别是原始值)包装在额外的实例中可能会降低性能。在紧密循环中,这可能会变得明显或更糟。

请注意,编译器可能能够规避 Optional 的短生命周期的额外引用。在 Java 10 中, 值类型 可能会进一步减少或消除此处的额外开销。

序列化

Optional不可序列化,但一个解决方案并不过于复杂。

不变性

由于Java中泛型类型的不变性,当实际值类型被推入泛型类型参数时,某些操作会变得繁琐。一个例子可以在这里(见“参数多态性”)找到。


1
另一个缺点(或限制)是您无法对Optional(xxx)实例进行排序,尽管Oracle明确地故意施加了这种限制。我对提供这些类可用性的智慧在特定情况下使用它们持怀疑态度,并假设开发人员会做正确的事情。解决一个问题可能会引起另一个问题。也许应该有编译器警告来帮助开发人员在“不良”方式中使用Optional(xxx)类(例如作为方法参数,在集合中,在构造函数中等),如果真的不是预期的用法? - skomisa
1
关于集合中的可选项:有时将可选项存储在List中是非常有用的。当某个元素位于特定索引处很重要,但该元素可能存在或不存在时,这种情况就会出现。这通过List<Optional<T>>类型清晰地传达。另一方面,如果仅重要的是哪些对象是某个集合的成员,则没有意义。 - Lii
7
我认为使用案例#2仍然存在疑问。传递到方法中的“Optional”可能是“null”。那么你得到了什么?现在你需要做两个检查。请参见https://dev59.com/slwZ5IYBdhLWcg3wC8f4#31923042。 - David V
4
@DavidV,看一下我列出的优点清单 - 这些都同样适用于那种情况。此外,如果您全面采用“Optional”(如果您愿意将其作为参数传递),则“null”永远不是合法值。因此,使用显式参数“null”调用方法显然是一个错误。 - Nicolai Parlog
1
“包装值(尤其是原始类型)…” —— 那 OptionalInt 呢?我不明白为什么将 int 包装在可空的 Integer 中比使用 OptionalInt 更糟糕。 - Guildenstern
@Guildenstern 你说得对,这并不比将其包装成 Integer 更糟。它们是一样的,这就是为什么在高性能情况下都要避免使用。 - Nicolai Parlog

43

就我个人而言,我更喜欢使用IntelliJ的代码检查工具来进行@NotNull@Nullable检查,因为这些检查主要在编译时进行(也可以进行一些运行时检查),这样可以减少代码可读性和运行时性能方面的开销。虽然它不像使用Optional那样严格,但这种缺乏严格性应该由良好的单元测试支撑。

public @Nullable Foo findFoo(@NotNull String id);

public @NotNull Foo doSomething(@NotNull String id, @Nullable Bar barOptional);

public class Book {

  private List<Pages> pages;
  private @Nullable Index index;

}

List<@Nullable Foo> list = ..

这适用于Java 5,不需要包装和拆包值(或创建包装对象)。


10
这些是IDEA注释吗?个人更喜欢JSR305的@ Nonnull -- 与FindBugs兼容。 - fge
1
@fge 在这方面缺乏标准,但我认为工具通常是可配置的,你不必最终使用 @Nonnull @NotNull 等。 - Peter Lawrey
6
没错,这是真的;IDEA(13.x)提供了三个不同的选择... 呃,反正我最后总是使用JSR 305。 - fge
4
我知道这是老帖子了,但有关注解和工具的讨论值得加上这个链接:https://dev59.com/jVsV5IYBdhLWcg3wsguh - 顺便说一下,这个链接专门解决了Java 8的情况。 - Stephan Herrmann
1
截至2021年01月29日,这应该再次获得192个赞。 - Ralf H

29

我认为Guava Optional和它们的维基页面已经说明得很清楚了:

给null取一个名称可以提高可读性,但Optional最大的优点是防傻。如果你想让程序编译通过,你必须积极思考缺失情况,因为你必须主动解包Optional并处理该情况。null使得忘记事情变得异常轻松,虽然FindBugs有所帮助,但我们认为它解决不了这个问题。

当你返回可能存在或不存在的值时,这一点尤其重要。你(和其他人)更容易忘记other.method(a, b)可能返回null值,而在实现other.method时,你更容易忘记a可能为空。返回Optional使调用者不可能忘记这种情况,因为他们必须自己解包对象才能编译他们的代码。 -- (来源:Guava维基 - 使用和避免null - 重点在哪里?)

Optional会增加一些开销,但我认为它的明显优势是使对象可能不存在变得明确,并强制程序员处理这种情况。它可以防止某人忘记喜欢的!= null检查。

2为例,写出来的代码更加清晰明了:

if(soundcard.isPresent()){
  System.out.println(soundcard.get());
}

than

if(soundcard != null){
  System.out.println(soundcard);
}

对我来说,Optional更好地表达了没有声卡存在的事实。

关于你的看法:

  1. public Optional<Foo> findFoo(String id); - 我不确定这个。也许我会返回一个Result<Foo>,其中可能是empty或包含一个Foo。这是一个类似的概念,但不是真正的Optional
  2. public Foo doSomething(String id, Optional<Bar> barOptional); - 我更喜欢@Nullable和findbugs检查,就像Peter Lawrey的答案中所示 - 请参见此讨论
  3. 你的书例子 - 我不确定我是否会在内部使用Optional,这可能取决于复杂性。对于一本书的“API”,我会使用Optional<Index> getIndex()来明确表示该书可能没有索引。
  4. 我不会在集合中使用它,而是不允许集合中的null值

总的来说,我会尽量减少传递null。 (曾经受过伤害...)我认为值得找到适当的抽象并告诉其他程序员某个返回值实际上代表什么。


21
你应该写soundcard.ifPresent(System.out::println)。调用 isPresent 在语义上与检查 null 是相同的。 - a better oliver
3
对我来说问题在于带有可选类型的事物仍然可以是 null。因此,要实际上完全安全,您必须执行诸如 if(soundcard != null && soundcard.isPresent()) 的操作。尽管创建一个返回 Optional 但也可能返回 null 的 API 是我希望没有人会这样做的事情。 - Simon Baumgardt-Wellander
1
如果你想让程序编译通过,它会强制你积极思考缺失的情况,因为你必须主动解包Optional并处理该情况。我不太确定它是如何强制执行这一点的,因为我并不经常使用Java。你能解释一下Java是如何强制你解包并检查!isPresent情况才能编译通过的吗? - crush
2
我从未理解的是:我们被期望忘记 !=null,但我们从未忘记 .isPresent? - Ralf H

19

来自Oracle教程:

Optional的目的不是替换代码库中每一个空引用,而是帮助设计更好的API,这样仅通过读取方法签名,用户就可以知道是否可以期望获得可选值。此外,Optional强制你主动解包Optional来处理缺失值,因此保护代码免受意外的空指针异常。


4
APIs仅用于方法的返回部分,对吗?由于类型擦除,选项不应作为参数使用?有时我在方法内部使用Optional,将其从可空参数转换而来或作为字段初始化从空构造函数参数 ... 仍在努力确定是否比将其保留为空更有用 - 你们有什么想法吗? - ycomp
1
@ycomp 你可以在这个或者那个问题中找到关于这些情况的很多信息。这些问题的答案告诉了比所问的更多,并涵盖了其他情况。如果需要更多信息,请浏览更多或提出自己的问题,因为在这里可能不会得到太多关注。;) - ctomek

16
在Java中,除非你迷恋函数式编程,否则不要使用Optional。它们不适合作为方法参数(我保证有一天会有人给你传递一个null的Optional,而不仅仅是一个空的Optional)。对于返回值,它们是有意义的,但是它们会引导客户端类继续扩展行为构建链。在命令式语言(如Java)中,FP和链条很少有用处,因为这使得调试变得非常困难,不仅仅是阅读。当你跳到代码行时,你无法知道程序的状态和意图;你必须跳到代码内部去弄清楚(通常是多个堆栈帧深度,尽管有步长过滤器),并且你必须添加许多断点来确保它可以停止在你添加的代码/lambda中,而不是简单地走if/else/call等琐碎的代码行。如果你想使用函数式编程,请选择Java之外的其他语言,并希望你拥有可调试的工具。

我完全同意。可选项只是一个花招。 - undefined

12

1 - 当方法可能返回null时,作为公共方法的返回类型:

这里有一篇好文章展示了使用案例#1的有用性。下面是代码:

...
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            isocode = isocode.toUpperCase();
        }
    }
}
...

被转换成这样

String result = Optional.ofNullable(user)
  .flatMap(User::getAddress)
  .flatMap(Address::getCountry)
  .map(Country::getIsocode)
  .orElse("default");

通过将Optional作为相应的getter方法的返回值。


5
我认为,第一个例子比转换后的例子更易读。而且第一个例子只需要一个if语句,而不是三个。 - OuuGiii
这个例子假设getter方法返回一个Optional对象。返回Optional对象的getter方法并不被任何人推荐,包括Oracle在内。 - undefined

3

这里有一个我认为很有趣的应用程序... 测试。

我打算对我的一个项目进行大量测试,因此我建立了断言; 只是有些事情我必须验证,有些事情我不必验证。

因此,我构建需要验证的事物,并使用assert语句来验证它们,如下所示:

public final class NodeDescriptor<V>
{
    private final Optional<String> label;
    private final List<NodeDescriptor<V>> children;

    private NodeDescriptor(final Builder<V> builder)
    {
        label = Optional.fromNullable(builder.label);
        final ImmutableList.Builder<NodeDescriptor<V>> listBuilder
            = ImmutableList.builder();
        for (final Builder<V> element: builder.children)
            listBuilder.add(element.build());
        children = listBuilder.build();
    }

    public static <E> Builder<E> newBuilder()
    {
        return new Builder<E>();
    }

    public void verify(@Nonnull final Node<V> node)
    {
        final NodeAssert<V> nodeAssert = new NodeAssert<V>(node);
        nodeAssert.hasLabel(label);
    }

    public static final class Builder<V>
    {
        private String label;
        private final List<Builder<V>> children = Lists.newArrayList();

        private Builder()
        {
        }

        public Builder<V> withLabel(@Nonnull final String label)
        {
            this.label = Preconditions.checkNotNull(label);
            return this;
        }

        public Builder<V> withChildNode(@Nonnull final Builder<V> child)
        {
            Preconditions.checkNotNull(child);
            children.add(child);
            return this;
        }

        public NodeDescriptor<V> build()
        {
            return new NodeDescriptor<V>(this);
        }
    }
}

在NodeAssert类中,我这样做:
public final class NodeAssert<V>
    extends AbstractAssert<NodeAssert<V>, Node<V>>
{
    NodeAssert(final Node<V> actual)
    {
        super(Preconditions.checkNotNull(actual), NodeAssert.class);
    }

    private NodeAssert<V> hasLabel(final String label)
    {
        final String thisLabel = actual.getLabel();
        assertThat(thisLabel).overridingErrorMessage(
            "node's label is null! I didn't expect it to be"
        ).isNotNull();
        assertThat(thisLabel).overridingErrorMessage(
            "node's label is not what was expected!\n"
            + "Expected: '%s'\nActual  : '%s'\n", label, thisLabel
        ).isEqualTo(label);
        return this;
    }

    NodeAssert<V> hasLabel(@Nonnull final Optional<String> label)
    {
        return label.isPresent() ? hasLabel(label.get()) : this;
    }
}

这意味着断言只有在我想要检查标签时才会触发!

2

Optional类允许避免使用null并提供更好的替代方案:

  • 这鼓励开发者做存在性检查,以避免未捕获的NullPointerException

  • API变得更加易于理解,因为我们可以看到哪些值可能不存在。

Optional为进一步处理对象提供了便捷的API: isPresent(); get(); orElse(); orElseGet(); orElseThrow(); map(); filter(); flatmap()

此外,许多框架积极使用此数据类型并从其API返回它。


2
一个 Optional 与 Iterator 设计模式的不可修改实例具有相似的语义:
  • 它可能引用或不引用对象(由 isPresent() 给出)
  • 如果它确实引用对象,则可以通过 get() 进行取消引用
  • 但是,它不能在序列中前进到下一个位置(它没有 next() 方法)。
因此,在以前考虑使用 Java Iterator 的情况下,请考虑返回或传递一个 Optional

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