为什么Java 8's Optional不应该在参数中使用

我在许多网站上读到过,Optional应该只用作返回类型,而不能用于方法参数中。我在努力寻找一个合乎逻辑的原因。例如,我有一个逻辑,它有两个可选参数。因此,我认为这样写我的方法签名是有意义的(解决方案1):

public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {
// my logic
}

许多网页指定Optional不应该用作方法参数。考虑到这一点,我可以使用下面的方法签名,并添加一个明确的Javadoc注释来指定参数可能为null,希望未来的维护者能够读取Javadoc,因此在使用参数之前总是执行null检查(解决方案2):

public int calculateSomething(String p1, BigDecimal p2) {
// my logic
}

或者,我可以用四个公共方法替换我的方法,以提供更好的接口,并使p1和p2是可选的(解决方案3):

public int calculateSomething() {
calculateSomething(null, null);
}


public int calculateSomething(String p1) {
calculateSomething(p1, null);
}


public int calculateSomething(BigDecimal p2) {
calculateSomething(null, p2);
}


public int calculateSomething(String p1, BigDecimal p2) {
// my logic
}

现在,我尝试编写为每种方法调用这段逻辑的类的代码。我首先从另一个返回Optionals的对象检索两个输入参数,然后调用calculateSomething。因此,如果使用解决方案1,调用代码将看起来像这样:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1, p2);

如果使用解决方案2,调用代码看起来像这样:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));

如果应用解决方案3,我可以使用上面的代码,也可以使用下面的代码(但它的代码明显更多):

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result;
if (p1.isPresent()) {
if (p2.isPresent()) {
result = myObject.calculateSomething(p1, p2);
} else {
result = myObject.calculateSomething(p1);
}
} else {
if (p2.isPresent()) {
result = myObject.calculateSomething(p2);
} else {
result = myObject.calculateSomething();
}
}

对我来说,这看起来是最有可读性的解决方案,并且对未来的维护者来说,参数可以为空/null是最明显的。(我知道Optional的设计人员只打算将它用作返回类型,但我找不到在这种情况下不使用它的任何逻辑理由)。

255985 次浏览
我认为这是因为您通常编写函数来操作数据,然后使用map和类似的函数将其提升到Optional。这将添加默认的Optional行为。 当然,在某些情况下,有必要编写自己的辅助函数,使其在Optional上工作

我相信存在的原因是你必须首先检查Optional本身是否为null,然后尝试计算它包装的值。太多不必要的验证。

使用Optional的模式是为了避免返回 null。仍然完全有可能将null传递给一个方法。

虽然这些还不是正式的,但您可以使用jsr - 308的风格注释来指示是否接受函数中的null值。请注意,您必须有正确的工具来实际识别它,它将提供更多的静态检查而不是可执行的运行时策略,但它将有所帮助。

public int calculateSomething(@NotNull final String p1, @NotNull final String p2) {}

哦,这些编码风格应该有所保留。

  1. (+)传递一个可选结果给另一个方法,没有任何语义分析;把它留给方法,是很好的。
  2. (-)在方法中使用可选参数导致条件逻辑是反作用的。
  3. (-)需要将参数打包到Optional中,对编译器来说是次优的,并且做了不必要的包装。
  4. (-)与可空参数相比,Optional的开销更大。
  5. (-)有人在实际参数中传递Optional为空的风险。

总的来说:可选的统一了两个状态,这两个状态必须被分解。因此,由于数据流的复杂性,它更适合于结果而不是输入。

这个建议是“对输入尽可能不具体,对输出尽可能具体”经验法则的一种变体。

通常,如果您有一个接受普通非空值的方法,您可以将它映射到Optional上,因此普通版本严格来说对输入更加不特定。尽管如此,你仍然需要Optional参数的原因有很多:

  • 你希望你的函数与另一个返回Optional的API一起使用
  • 如果给定值为空,则函数应该返回除空的Optional之外的其他内容
  • 你认为Optional太棒了,以至于任何使用你的API的人都应该被要求学习它;-)

这对我来说似乎有点傻,但我能想到的唯一原因是方法参数中的对象参数在某种程度上已经是可选的——它们可以为空。因此,强迫某人使用现有对象并将其包装为可选对象是毫无意义的。

也就是说,将可选的take/return方法链接在一起是一件合理的事情,例如monad。

正如Brian Goetz所解释的那样,可选项并不是为此目的而设计的。

您总是可以使用@Nullable来表示方法参数可以为空。使用可选方法并不能让你的方法逻辑写得更整齐。

几乎没有理由不使用Optional作为参数。反对这一点的论点依赖于权威的论点(见Brian Goetz -他的论点是我们不能强制非空可选项)或者Optional参数可能是空的(本质上是相同的论点)。当然,Java中的任何引用都可以为空,我们需要鼓励规则由编译器执行,而不是由程序员的内存执行(这是有问题的,并且不能扩展)。

函数式编程语言鼓励使用Optional参数。使用它的最佳方法之一是有多个可选参数,并使用liftM2来使用一个函数,假设参数不是空的并返回一个可选参数(参见http://www.functionaljava.org/javadoc/4.4/functionaljava/fj/data/Option.html#liftM2-fj.F-)。不幸的是,Java 8实现了一个非常有限的库,支持可选功能。

作为Java程序员,我们应该只使用null来与遗留库交互。

我看到的关于这个话题的最好的文章是由丹尼尔Olszewski写的:

尽管对于非强制性的方法参数考虑Optional可能很有吸引力,但与其他可能的替代方案相比,这样的解决方案显得苍白无力。为了说明这个问题,检查以下构造函数声明:

public SystemMessage(String title, String content, Optional<Attachment> attachment) {
// assigning field values
}
乍一看,这可能是一个正确的设计决策。毕竟,我们 显式地将附件参数标记为可选。然而,随着 对于调用构造函数,客户端代码可能变得有点 笨拙。< / p >
SystemMessage withoutAttachment = new SystemMessage("title", "content", Optional.empty());
Attachment attachment = new Attachment();
SystemMessage withAttachment = new SystemMessage("title", "content", Optional.ofNullable(attachment));
选项的工厂方法不是提供清晰度,而是Optional的工厂方法 上课只会分散读者的注意力。注意只有一个可选选项 参数,但是想象一下有两个或三个。鲍勃叔叔 不会为这样的代码感到骄傲😉

当一个方法可以接受可选参数时,最好采用经过验证的方法,用方法设计这种情况 重载。在SystemMessage类的例子中,声明 两个单独的构造函数优于使用Optional.

public SystemMessage(String title, String content) {
this(title, content, null);
}


public SystemMessage(String title, String content, Attachment attachment) {
// assigning field values
}

这一变化使得客户端代码更简单,更易于阅读。

SystemMessage withoutAttachment = new SystemMessage("title", "content");
Attachment attachment = new Attachment();
SystemMessage withAttachment = new SystemMessage("title", "content", attachment);

还有一种方法,你能做的是

// get your optionals first
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();


// bind values to a function
Supplier<Integer> calculatedValueSupplier = () -> { // your logic here using both optional as state}

一旦你构建了一个函数(在本例中是供应商),你就可以将它作为任何其他变量传递,并能够使用它来调用

calculatedValueSupplier.apply();

这里的想法是,你是否有可选值将是你的函数的内部细节,而不是参数。将可选参数作为参数来考虑函数实际上是我发现的非常有用的技巧。

至于你的问题,你是否应该这样做,这是基于你的偏好,但正如其他人所说,它使你的API丑陋,至少可以说。

我的看法是,可选的应该是一个单子,这些在Java中是不可想象的。

在函数式编程中,你要处理的是纯函数和高阶函数,它们只根据“业务域类型”获取和组合参数。组合函数,或其计算应该报告给,现实世界(所谓的副作用),需要应用函数,负责自动解包的值从单子表示外部世界(状态,配置,期货,Maybe, Either, Writer,等等…);这叫做抬举。你可以把它看作是一种关注点的分离。

混合使用这两种抽象级别不利于可读性,所以最好避免使用。

在传递Optional作为参数时要小心的另一个原因是方法应该做一件事…如果你传递一个Optional参数,你可以做不止一件事,它可以类似于传递一个布尔参数。

public void method(Optional<MyClass> param) {
if(param.isPresent()) {
//do something
} else {
//do some other
}
}

起初,我也倾向于将optional作为参数传递,但如果从API-Designer透视图切换到API-User透视图,就会看到缺点。

对于你的例子,每个参数都是可选的,我建议将计算方法更改为一个自己的类,如下所示:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();


MyCalculator mc = new MyCalculator();
p1.map(mc::setP1);
p2.map(mc::setP2);
int result = mc.calculate();

我知道这个问题更多的是意见而不是事实。但是我最近从一个。net开发人员转变为java开发人员,所以我最近才加入了可选的一方。# EYZ0

我一直在做的,这是我的经验之谈。是使用可选项作为返回类型,并且仅使用可选项作为参数,如果我需要可选项的值,以及可选项是否在方法中有值。

如果我只关心值,我在调用方法之前检查isPresent,如果我在方法中有某种日志记录或不同的逻辑,这取决于值是否存在,那么我将愉快地传入Optional。

接受Optional作为参数会在调用者级别引起不必要的包装。

例如:

public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {}

假设您有两个非空字符串(即。从其他方法返回):

String p1 = "p1";
String p2 = "p2";

即使您知道它们不是空的,也必须将它们包装在Optional中。

当你必须与其他“可映射”结构组合时,情况会变得更糟。# EYZ0:

Either<Error, String> value = compute().right().map((s) -> calculateSomething(
< here you have to wrap the parameter in a Optional even if you know it's a
string >));

裁判:

方法不应该期望Option作为参数,这几乎总是一个 指示从调用方到的控制流泄漏的代码气味 对于被呼叫者,应由呼叫者负责检查 选项的内容

ref。# EYZ0

这是因为我们对API用户和API开发人员有不同的要求。

开发人员负责提供精确的规范和正确的实现。因此,如果开发人员已经意识到一个参数是可选的,那么实现必须正确地处理它,无论它是null还是optional。API应该对用户尽可能简单,null是最简单的。

另一方面,结果从API开发人员传递给用户。尽管规范是完整和冗长的,但仍然有可能用户没有意识到它或只是懒得处理它。在这种情况下,Optional结果迫使用户编写一些额外的代码来处理可能为空的结果。

首先,如果你使用方法3,你可以用下面的代码替换最后14行代码:

# EYZ0

您编写的四个变体是方便方法。你应该只在更方便的时候使用它们。这也是最好的方法。这样,API就非常清楚哪些成员是必要的,哪些不是。如果你不想写四个方法,你可以通过参数的命名来澄清:

# EYZ0

这样,就很清楚允许空值。

您使用p1.orElse(null)说明了我们的代码在使用Optional时是多么冗长,这也是我避免使用它的部分原因。Optional是为函数式编程而编写的。流需要它。你的方法可能永远不应该返回Optional,除非在函数式编程中有必要使用它们。有一些方法,比如Optional.flatMap()方法,需要对返回Optional的函数的引用。这是它的签名:

# EYZ0

这通常是编写返回Optional的方法的唯一理由。但即使在那里,也可以避免。您可以将一个不返回Optional的getter传递给flatMap()这样的方法,方法是将它包装在另一个将函数转换为正确类型的方法中。包装器方法看起来像这样:

public static <T, U> Function<? super T, Optional<U>> optFun(Function<T, U> function) {
return t -> Optional.ofNullable(function.apply(t));
}

假设你有一个这样的getter: String getName()

你不能像这样把它传递给flatMap:

# EYZ0

但你可以这样传递:

# EYZ0

在函数式编程之外,应该避免使用可选项。

Brian Goetz说得很好:

将Optional添加到Java的原因是:

return Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.stream()
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
.filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.findFirst()
.getOrThrow(() -> new InternalError(...));

比这个更干净:

Method matching =
Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.stream()
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
.filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.getFirst();
if (matching == null)
throw new InternalError("Enclosing method not found");
return matching;

查看JDK10中的JavaDoc, https://docs.oracle.com/javase/10/docs/api/java/util/Optional.html,添加了一个API注释:

注意:< p > API 可选类型主要用于明确需要表示“没有结果”的方法返回类型,并且使用null可能会导致错误

让我们明确一点:在其他语言中,并没有反对将Maybe类型用作字段类型、构造函数形参类型、方法形参类型或函数形参类型的一般建议。

所以如果你"不应该"在Java中使用Optional作为参数类型,原因是具体的到Optional,到Java,或者两者都是。

可能适用于其他Maybe类型或其他语言的推理在这里可能是无效的。

/ # EYZ0,

[W]e确实有一个明确的 添加[可选]时的意图,而不是一般性的 也许是类型,就像很多人希望我们做的那样 所以。我们的意图是为图书馆提供一个有限的机制 方法返回需要以明确方式表示的类型 “no result”,使用null来表示这样的结果是压倒性的 导致错误。< / p > 例如,你可能永远不应该将它用于 返回结果数组或结果列表;而是返回 空数组或列表。你几乎不应该把它用作

所以答案是针对Optional的:它不是“一般用途的可能类型”;因此,它是有限制的,而且限制的方式可能限制了它作为字段类型或参数类型的用处。

也就是说,在实践中,我很少发现使用Optional作为字段类型或参数类型是一个问题。如果Optional(尽管有局限性)作为参数类型或字段类型用于您的用例,请使用它。

也许我会引起一堆反对票和负面评论,但是……我站不起来了。

免责声明:我在下面写的并不是对最初问题的回答,而是我对这个话题的想法。它的唯一来源是我的想法和经验(使用Java和其他语言)。

首先让我们看看,为什么会有人喜欢使用可选?

对我来说,原因很简单:与其他语言不同,java没有内置的功能来定义变量(或类型)为空或非空。所有的“对象”变量都是可空的,而所有的原语类型都不是。为了简单起见,在进一步的讨论中不要忽略原始类型,因此我将简单地声明所有变量都是可空的。

为什么需要将变量声明为可空/非可空?我的理由是:显式总是比隐式好。此外,显式修饰(例如注释或类型)可以帮助静态分析器(或编译器)捕获一些与空指针相关的问题。

许多人在上面的评论中争论说,函数不需要有可空参数。相反,应该使用重载。但这样的说法只能出现在教科书上。在现实生活中有不同的情况。考虑类,它表示一些系统的设置,或者一些用户的个人数据,或者实际上任何组合数据结构,它包含许多字段——其中许多具有重复的类型,一些字段是必选的,而另一些字段是可选的。在这种情况下,继承/构造函数重载并没有真正的帮助。

随机的例子:假设,我们需要收集关于人的数据。但有些人不想提供所有的数据。当然这是POD,基本上是type with value-semantics,所以我希望它或多或少是不可变的(没有setter)

class PersonalData {
private final String name; // mandatory
private final int age; // mandatory
private final Address homeAddress; // optional
private final PhoneNumber phoneNumber; // optional. Dedicated class to handle constraints
private final BigDecimal income; // optional.
// ... further fields


// How many constructor- (or factory-) overloads do we need to handle all cases
// without nullable arguments? If I am not mistaken, 8. And what if we have more optional
// fields?


// ...
}

因此,IMO上面的讨论表明,即使大多数情况下我们可以在没有可空参数的情况下生存,但有时它实际上是不可行的。

现在我们来看看问题:如果一些参数是可空的,而另一些不是,我们怎么知道是哪个?

方法1:所有参数都是可空的(根据java标准,除了基本类型)。所以我们检查了所有的。

结果:代码因检查而爆炸,这些检查大多是不需要的,因为正如我们上面所讨论的,几乎所有时间我们都可以使用可为空的变量,只有在极少数情况下才能使用“nullables"是必要的。

方法2:使用文档和/或注释来描述,哪些参数/字段是可空的,哪些不是。

结果:它实际上不起作用。人们懒得写和读文档。此外,最近的趋势是,我们应该避免编写文档,而倾向于使代码本身自描述。此外,所有关于修改代码和忘记修改文档的理由仍然有效。

方法3:@Nullable @NonNull等…我个人觉得他们很好。但也有一定的缺点:(例如,它们只被外部工具尊重,而不是编译器),其中最糟糕的是,它们不是标准的,这意味着,1。我需要向我的项目添加外部依赖,以从中受益。不同制度对待他们的方式并不统一。据我所知,它们被投票排除在官方Java标准之外(我不知道是否有再次尝试的计划)。

方法4:可选的。缺点已经在其他评论中提到,其中最糟糕的是(IMO)性能惩罚。它还添加了一些样板文件,尽管我个人认为,使用Optional.empty()和Optional.of()并不是那么糟糕。优点是显而易见的:

  1. 它是Java标准的一部分。
  2. 对于代码的读者(或API的用户)来说,这些参数可能是空的。此外,它还强制API的用户和方法的开发人员通过显式地包装/展开值来确认这一事实(当使用@Nullable等注释时,情况并非如此)。

所以在我看来,任何方法论都不存在非黑即白的问题,包括这个方法。我个人总结了以下准则和约定(仍然不是严格的规则):

  1. 在我自己的代码中,所有变量都必须为非空(但可能是Optional<>)。
  2. 如果我有一个方法与一个或两个可选参数,我尝试重新设计它使用重载,继承等。
  3. 如果我不能在合理的时间内找到解决方案,我就会开始考虑性能是否关键(即是否有数百万个对象需要处理)。通常情况并非如此。
  4. 如果不是,我使用Optional作为参数类型和/或字段类型。

但仍有一些灰色地带,在那里这些惯例并不奏效:

  • 我们需要高性能(例如,处理大量数据,因此总执行时间非常长,或者吞吐量非常关键的情况)。在这种情况下,由Optional引入的性能惩罚可能真的是不需要的。
  • 我们在代码的边界上,这些代码是我们自己写的,例如:我们从DB、Rest Endpoint、解析文件等读取。
  • 或者我们只是使用一些外部库,它们不遵循我们的约定,所以,我们应该再次小心……

顺便说一下,后两种情况也可以作为可选字段/参数的需求来源。例如,当数据的结构不是我们自己开发的,而是由一些外部接口,db-schema等强加的……

最后,我认为,人们应该思考正在解决的问题,并尝试找到合适的工具。如果Optional<比;是合适的,那么我没有理由不使用它。

编辑:方法5:当我不能使用Optional时,我最近使用了这个方法。其思想很简单,就是对方法参数和类变量使用命名约定。我使用了&;maybe&;-前缀,这样如果例如&;url"参数为空,则它变成maybeUrl。它的优点是略微提高了意图的可理解性(并且没有其他方法的缺点,如外部依赖或性能损失)。但是也有缺点,比如:没有工具来支持这个约定(如果你没有先检查就访问了"maybe"-变量,你的IDE将不会显示任何警告)。另一个问题是,只有当所有参与项目的人一致地应用它时,它才会有所帮助。

在一些涉及protobufs或在配置对象中设置字段的用例中,使用Optional作为参数可能很有用。

public void setParameters(Optional<A> op1, Optional<B> op2) {
ProtoRequest.Builder builder = ProtoRequest.newBuilder();
op1.ifPresent(builder::setOp1);
op2.ifPresent(builder::setOp2);
...
}
我认为在这种情况下,用optional作为参数可能会有用。接收原型请求的API将处理不同的字段。 如果函数没有对这些参数进行额外的计算,那么使用Optional可能会更简单
public void setParameters(A op1, B op2) {
ProtoRequest.Builder builder = ProtoRequest.newBuilder();
if (op1 != null) {
builder.setOp1(op1);
}
if (op2 != null) {
builder.setOp2(op2);
}
...
}

不管Java 8,使用老派的方法重载技术带来清晰和灵活性,假设你有以下两个参数的方法

public void doSomething(arg1,arg2);

如果你想添加额外的可选参数,那么重载方法

public void doSomething(arg1,arg2,arg3) {
Result result = doSomething(arg1,arg2);
// do additional working
}

一个很好的例子是可选的参数会很好是JPA存储库。我喜欢做一些像findbynameand姓氏(可选,可选)。这样,如果Optional为空,则不执行WHERE param=y