可选用途

我已经使用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有什么好的用途吗?

有我错过的案子吗?

140827 次浏览

似乎只有当Optional中的类型T是一个基本类型,如intlongchar等时,Optional才有用。对于“真正的”类,它对我来说没有意义,因为你可以使用null值。

我认为它是从这里(或从另一个类似的语言概念)。

Nullable<T>

在c#中,Nullable<T>很久以前就被引入来包装值类型。

就我个人而言,我更喜欢使用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,不需要包装和展开值。(或创建包装器对象)

我认为Guava Optional和他们的维基页面说得很好:

除了给null赋一个名字带来的可读性增加之外,Optional的最大优势是它的傻瓜性。如果您希望程序能够编译,就必须主动考虑不存在的情况,因为您必须主动展开Optional并处理这种情况。Null很容易让人忘记事情,尽管FindBugs有所帮助,但我们认为它不能很好地解决这个问题。

当您返回的值可能是“当前的”,也可能不是“当前的”时,这一点尤其相关。你(和其他人)更有可能忘记对方。Method (a, b)可能会返回一个空值,当你在实现other.method时,你很可能会忘记a可能是空值。返回Optional使调用者不可能忘记这种情况,因为他们必须自己打开对象以便代码编译。 ——(来源:番石榴维基-使用和避免null -什么是重点?)

Optional增加了一些开销,但我认为它的明显优势是使它显式 一个对象可能不存在,它强制程序员处理这种情况。它防止某人忘记心爱的!= null检查

2为例,我认为它是更明确的代码:

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

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

对我来说,Optional更好地捕捉了没有声卡的事实。

我对你的观点有2个看法:

  1. public Optional<Foo> findFoo(String id); -我不确定这一点。也许我将返回Result<Foo>,它可能是或包含Foo。它是一个类似的概念,但不是真正的Optional
  2. public Foo doSomething(String id, Optional<Bar> barOptional); -我更喜欢@Nullable和findbugs检查,就像在Peter Lawrey的回答是 -也请参阅这个讨论
  3. 你的书的例子-我不确定我是否会在内部使用可选的,这可能取决于复杂性。对于一本书的“API”,我将使用Optional<Index> getIndex()显式地指出这本书可能没有索引。
  4. 我不会在集合中使用它,而是不允许在集合中使用空值
一般来说,我会尽量减少传递__abc0。(一旦烧…) 我认为有必要找到适当的抽象,并向程序员同事指出某个返回值实际代表什么

Optional的主要设计目标是为函数返回值提供一种方法,以指示没有返回值。看到这个讨论这允许调用方继续一连串流畅的方法调用。

这与OP问题中的用例# 1最接近。虽然,缺少值是一个比更精确的公式,因为像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");

这确实有局限性,但它比上面任何一种都要好得多。

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

处理Optional中缺少值的方法有三种:提供替代值、调用函数提供替代值或抛出异常。如果你要存储到一个字段中,你会在初始化或赋值时这样做。如果你要向列表中添加值,就像OP提到的那样,你可以选择不添加值,从而“扁平化”。剔除缺失值。

我相信有人会想出一些虚构的情况,他们真的想在字段或集合中存储Optional,但一般来说,最好避免这样做。

这里有一个关于……的有趣用法(我认为)。测试。

我打算对我的一个项目进行大量测试,因此我构建断言;只是有些事我必须核实,有些事我不需要。

因此,我构建了一些东西来断言,并使用断言来验证它们,如下所示:

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;
}
}

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

虽然我来晚了,但无论如何,我还是想多说几句。它们违背了Optional的设计目标,这是由斯图尔特·马克斯的回答很好地总结出来的,但我仍然相信它们的有效性(显然)。

到处使用Optional

在一般情况下

我写了一个完整的关于使用Optional的博客文章,但它基本上归结为:

  • 在设计类时,尽可能避免可选性
  • 在所有其他情况下,默认应该使用Optional而不是null
  • 可能为以下情况做例外:
    • 局部变量
    • 返回值和参数给私有方法
    • 性能关键代码块(不用猜测,使用分析器)

前两个异常可以减少在Optional中包装和解包装引用的可感知开销。它们的选择使得null对象永远不能合法地将边界从一个实例传递到另一个实例。

注意,这几乎永远不会允许Optionals出现在集合中,这几乎和nulls一样糟糕。千万别这么做。;)

关于你的问题

  1. 是的。
  2. 如果没有重载选项,则是。
  3. 如果没有其他方法(子类化、装饰……),可以。
  4. 请不!

优势

这样做会减少代码库中__abc0的存在,尽管它不会根除它们。但这还不是重点。还有其他重要的优势:

澄清意图

使用Optional清楚地表示该变量是可选的。任何代码的读者或API的消费者都会被这样的事实所迷惑,即可能没有任何内容,在访问值之前必须进行检查。

消除不确定性

没有Optional,出现null的意义是不清楚的。它可以是一个状态的合法表示(参见Map.get),也可以是一个实现错误,如缺少或失败的初始化。

随着Optional的持续使用,这种情况发生了巨大的变化。在这里,null的出现已经表明存在bug。(因为如果允许缺少该值,则会使用Optional。)这使得调试空指针异常变得容易得多,因为null的意义问题已经得到了回答。

更多空检查

现在没有东西可以再null了,这可以在任何地方强制执行。无论是使用注释、断言还是普通检查,您都不必考虑这个参数或那个返回类型是否可以为空。它不能!

缺点

当然,没有什么灵丹妙药……

性能

将值(尤其是原语)包装到额外的实例中会降低性能。在紧密循环中,这可能会变得明显,甚至更糟。

请注意,编译器可能能够规避__abc0的短生命周期的额外引用。在Java 10中值类型可能会进一步减少或删除惩罚。

序列化

Optional是不可序列化的解决方案并不太复杂。

不变性

由于Java中泛型类型的不变性,当实际值类型被推入泛型类型参数时,某些操作变得很麻烦。给出了一个例子这里(参见“参数化多态性”;)

甲骨文教程:

Optional的目的不是替换代码库中的每一个空引用,而是帮助设计更好的api,使用户可以通过读取方法的签名来判断是否需要一个可选值。此外,Optional强制您主动展开Optional以处理缺少值的情况;因此,可以保护代码不受意外空指针异常的影响。

我不认为Optional是可能返回空值的方法的一般替代品。

基本思想是:一个值的缺失并不意味着它在未来可能是可用的。它是findById(-1)和findById(67)之间的差值。

可选项对于调用者的主要信息是,他可能不指望给定的值,但它可能在某个时间可用。也许它会再次消失,然后再回来一次。这就像一个开/关开关。你可以“选择”打开或关闭灯。但是如果你没有灯可以打开,你就别无选择了。

所以我发现它太乱引入可选的地方,以前可能返回null。我仍然会使用null,但是只在一些受限的地方,比如树的根、惰性初始化和显式查找方法。

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();
}
}
}
...

转换为this

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

通过使用Optional作为各自getter方法的返回值。

Optional类让你避免使用null,并提供了一个更好的选择:

  • 这鼓励开发人员检查是否存在,以避免未捕获的NullPointerException

  • API有了更好的文档记录,因为它可以看到,在哪里可以期望那些可能缺失的值。

Optional为进一步处理对象提供了方便的API: isPresent();get();get()0;get()1;get()2;get()3;get()4;get()5 . < / p >

此外,许多框架积极地使用这种数据类型并从它们的API中返回它。

Optional 与迭代器设计模式的不可修改实例具有相似的语义:

  • 它可能引用也可能不引用对象(由isPresent()给出)
  • 如果它确实引用了一个对象,则可以解除引用(使用get())
  • 但它不能被提升到序列中的下一个位置(它没有next()方法)。

因此,请考虑在以前可能考虑使用Java Iterator的上下文中返回或传递Optional

在java中,除非你沉迷于函数式编程,否则不要使用它们。

它们没有作为方法参数的位置(我保证有一天会有人传递给你一个空的可选参数,而不仅仅是一个空的可选参数)。

它们对返回值有意义,但它们会让客户端类继续延伸行为构建链。

FP和链在像java这样的命令式语言中几乎没有位置,因为它使调试变得非常困难,而不仅仅是阅读。当你走到这一行时,你无法知道程序的状态和意图;你必须一步一步地找出它(尽管有步骤过滤器,但通常不是你的代码和许多堆栈帧深),你必须添加大量断点,以确保它可以停止在你添加的code/lambda中,而不是简单地走if/else/call琐碎的行。

如果你想要函数式编程,选择java以外的语言,并希望你有调试它的工具。

下面是一些你可以在Optional<T>实例上执行的方法:

  • map
  • flatMap
  • orElse
  • orElseThrow
  • ifPresentOrElse
  • get

下面是你可以在null上执行的所有方法:

  • (没有)

这实际上是一个苹果和橘子的比较:Optional<T>是一个对象的实际实例(除非它是null…但这可能是一个错误),而null是一个中止的对象。使用null所能做的就是检查它是否真的是null。因此,如果你喜欢在对象上使用方法,Optional<T>适合你;如果你喜欢在特殊的字面量上进行分支,null适合你。

null不构成。您不能简单地组合一个只能进行分支的值。但是Optional<T>确实作曲。

例如,你可以使用map创建任意长链的“apply this function if non-empty”。或者,你可以使用ifPresent有效地创建一个命令代码块,如果可选值非空,则使用该可选值。或者你可以通过使用ifPresentOrElse来创建一个“if/else”,如果非空则使用非空可选参数,否则执行其他代码。

在这一点上,我们遇到了语言的真正限制,在我看来:对于非常令行禁止的代码,你必须将它们包装在lambdas中,并将它们传递给方法:

    opt.ifPresentOrElse(
string -> { // if present...
// ...
}, () -> { // or else...
// ...
}
);

这对某些人来说可能不够好,就风格而言。

如果Optional<T>是一个可以进行模式匹配的代数数据类型(这显然是伪代码:

    match (opt) {
Present(str) => {
// ...
}
Empty =>{
// ...
}
}

但无论如何,总结一下:Optional<T>是一个非常健壮的空或现对象。null只是一个前哨值。

主观上忽视了原因

似乎有一些人认为,效率应该决定是否应该使用Optional<T>或在null哨点值上进行分支。这似乎有点像在一般情况下对何时创建对象而不是原语制定硬性规则。我认为使用作为这个讨论的起点有点荒谬,因为你已经在一种语言中工作,在这种语言中,它习惯地使对象从左到右,从上到下,一直(在我看来)。

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

这是Optional的预期用例,如JDK API文档所示:

可选主要用于作为方法返回类型,其中 很明显,有必要表示“没有结果”;以及使用null

Optional表示两种状态之一:

  1. 它有一个值(isPresent返回true)
  2. 它没有值(isEmpty返回true)

因此,如果你有一个方法返回一些东西或不返回,这是Optional的理想用例。

这里有一个例子:

Optional<Guitarist> findByLastName(String lastName);

此方法接受一个用于在数据库中搜索实体的参数。有可能不存在这样的实体,因此使用Optional返回类型是一个好主意,因为它迫使调用该方法的人考虑空场景。这减少了NullPointerException. conf的可能性。

2 -当参数可以为空时,作为方法参数:

尽管在技术上是可能的,但这不是Optional的预期用例。

让我们考虑一下您提议的方法签名:

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

主要问题是我们可以调用doSomething,其中barOptional有三种状态之一:

  1. 一个带有值的Optional,例如doSomething("123", Optional.of(new Bar())
  2. 一个空的Optional,例如doSomething("123", Optional.empty())
  3. null,例如doSomething("123", null)

这3种状态需要在方法实现中适当地处理。

更好的解决方案是实现重载方法。

public Foo doSomething(String id);


public Foo doSomething(String id, Bar bar);

这使得API的使用者非常清楚调用哪个方法,并且不需要传递null

3 -作为bean的可选成员:

给定你的例子Book类:

public class Book {
private List<Pages> pages;
private Optional<Index> index;
}

Optional类变量遇到了与上面讨论的Optional方法参数相同的问题。它可以有三种状态之一:present、empty或null

其他可能的问题包括:

  • 序列化:如果你实现了Serializable并试图序列化这个类的对象,你会遇到一个java.io.NotSerializableException,因为Optional不是为这个用例设计的
  • 转换为JSON:当序列化为JSON时,Optional字段可能会以不希望的方式映射,例如{"empty":false,"present":true}。 虽然如果你使用流行的杰克逊库,它会对这个问题执行提供解决方案

尽管存在这些问题,Oracle自己在2014年发布Java 8 Optional时发布了这篇博文。它包含使用Optional作为类变量的代码示例。

public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}

在接下来的几年里,开发人员找到了更好的替代方法,例如实现getter方法来创建Optional对象。

public class Book {
private List<Pages> pages;
private Index index;
public Optional<Index> getIndex() {
return Optional.ofNullable(index);
}
}

在这里,我们使用ofNullable方法返回一个带有值的Optional,如果index是非空的,或者返回一个空的Optional

4 -收集:

我同意创建OptionalList(例如List<Optional<Foo>>)不会添加任何东西。 相反,如果List中不存在该项,则不将其包含在List