为什么在参数中不使用Java 8的Optional


392

我已经在许多网站上阅读了Optional应该仅用作返回类型,而不能在方法参数中使用。我正在努力找到一个合理的理由。例如,我有一段逻辑,其中包含2个可选参数。因此,我认为这样写我的方法签名是有意义的(解决方案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();
    }
}

所以我的问题是:为什么将Optionals用作方法参数被认为是不好的做法(请参阅解决方案1)? 对于我来说,这似乎是最易读的解决方案,并且对于将来的维护者来说,最明显的是参数可以为空/空。(我知道设计人员Optional希望将其仅用作返回类型,但在这种情况下我找不到任何不使用它的逻辑原因)。


16
如果您使用了可选参数,您是否不必检查作为参数传递的可选参数是否不是null
biziclop 2015年

17
是的,但是对于将来维护代码的其他人来说,该参数可以为空/空,因此有可能避免将来出现空指针异常
Neil Stevens

1
哇。是否将空参数传递给方法?这就是Visual Basic。违反什么原则?SRP(也许)。它也违反了另一个原则,该原则的名字使我丧失了沿途传递仅传递方法或函数执行其工作所需的信息的思路。
发光的

20
理论上所有可能为null的东西都类似于库中可能调用System.exit(0)的每个方法。您不能对此进行检查,也不应对此进行检查。实际上,您将一直(几乎)永远都不会做的所有事情。就像将所有参数都设为最终值一样。让您的工具帮助您防止更改参数值或忘记初始化字段,而不是使代码不被成千上万的终结符和许多空检查所读取。
yeoman

6
实际上,仅使用NonNull / Nullable注释,这就是您在这种情况下要寻找的,而不是可选的。
Bill K

Answers:


202

哦,这些编码风格要花点儿力气。

  1. (+)将Optional结果传递给另一种方法,而无需任何语义分析;将其留给方法,是完全可以的。
  2. (-)使用可选参数在方法内部引起条件逻辑实际上是相反的。
  3. (-)需要将参数打包在Optional中,对于编译器来说不是最佳选择,并且不需要进行包装。
  4. (-)与可为空的参数相比,可选的成本更高。

一般而言:Optional统一了两个状态,必须将其拆散。因此,对于数据流的复杂性,比输入更适合结果。


38
实际上,具有Optional参数表示以下三种状态之一:null可选,Optional带有实值isPresent() == false的非null和Optional包装实际值的非null 。
biziclop 2015年

16
@biziclop是的,不可避免的观点已经受到批评。但目的是仅具有非null表达式。没多久,在某些情况下也听到建议避免使用Optional的说法颇具讽刺意味。一@NotNull Optional
朱普·艾根

80
@biziclop请注意,如果您正在使用Optional,状态1(null可选)通常表示编程错误,因此您也可能不处理它(只是让它抛出NullPointerException
user253751

50
“对于编译器而言不是最理想的”,“与可为空的参数相比,“可选”的开销更大”-这些参数可能对C语言有效,而对Java语言无效。Java程序员应该将重点放在干净的代码,可移植性,可测试性,良好的体系结构,模块化等方面,而不是放在“可选的空引用比空引用更昂贵”上。而且,如果您发现需要专注于微优化,那么最好跳过对象,列表,泛型并切换到数组和基元(我不想在这里冒犯,我只是在分享我的观点) 。
Kacper86 '16

15
“对于编译器而言不是最理想的”:过早的优化是万恶之源...如果您不是在编写对性能有严格要求的代码(在尝试指定干净接口时通常是这种情况),则不必担心。
Mohan

165

我在该主题上看到的最好的帖子Daniel Olszewski写的:

尽管对于非强制性方法参数考虑“可选”可能很诱人,但与其他可能的选择相比,这种解决方案显得苍白。为了说明此问题,请检查以下构造函数声明:

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

乍一看,它可能是一个正确的设计决策。毕竟,我们将附件参数明确标记为可选。但是,对于调用构造函数,客户端代码可能会变得有些笨拙。

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

10
当只有一两个段落是相关的时,这是很多复制+粘贴操作。
Garret Wilson

47
不幸的是,这种解释并没有解决呼叫者何时解析(例如解析某些信息)的问题。使用方法重载(如上面的建议),调用代码必须说:“ X是否存在?如果这样,我将调用重载方法A。是否存在Y?我将不得不调用重载方法B。如果X和Y存在,我将不得不调用重载方法C。” 等等。对此可能有一个很好的答案,但是对“为什么”的解释并没有涵盖它。
加勒特·威尔逊

9
同样,对于集合,空集合与无集合之间也有明显的区别。例如,缓存。是缓存未命中吗?空可选/ null。是否有一个缓存命中,碰巧是一个空集合?完整的可选/集合。
Ajax

1
@Ajax我认为您误会了这篇文章。他们引用的是《有效Java》一书所提倡的观点,该观点指出,当方法返回类型是Collection时(不是任何对象,因为缓存将返回),那么您应该更喜欢返回一个空集合而不是nullor Optional
吉利

4
当然,您应该支持它,但是您并不总是可以控制某些代码,从真正的意义上讲,您可能想要区分“有一个值,它是一个空集合”与“没有一个值”之间的区别。值(尚未定义)”。由于“魔术无效”也是一种不鼓励的做法,因此,开发人员应自行决定选择最不糟糕的选择。我更喜欢用empty optional表示一个缓存未命中而不是一个空集合,因为实际的缓存值可能一个空集合。
Ajax

86

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

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

作为Java程序员,我们应该只使用null与旧版库进行交互。


3
如何使用@Nonnull@Nullable从javax.annotation中呢?我在开发的库中广泛使用了它们,并且IDE(IntelliJ)提供的支持非常好。
罗杰里奥

1
这是好的,但你必须到处使用此批注,你需要一个库来支持方法,如地图,flatMap /绑定,liftM2,顺序等
马克·佩里

14
函数式编程语言鼓励使用可选参数。需要引用。大多数功能不应该是可选的。取而代之的是,处理(使用适当的高阶函数)是否存在值的责任在于所讨论函数的调用方。
jub0bs 2016年

5
这个。确实,没有一个答案能令人信服,并且没有一个答案可以回答这个特定的问题:“为什么将可选参数用作方法参数被认为是不好的做法,而@NonNull如果用不同的方式服务于相同的目的,则使用方法方法注释完全可以吗?” 对我来说,只有一个参数至少具有某种意义:“应该使用可选参数来提供更好的具有明确返回类型的API。但是,对于方法参数,可以使用重载函数。” -尽管如此,我认为这不是足够强的论点。
UtkuÖzdemir'16

1
当然,最好将其设置为null,但是更可取的是首先不需要很多可选值,因为好的设计:D
yeoman

12

此建议是“对于输入尽可能不具体而对输出尽可能不具体”的经验法则的变体。

通常,如果您有一个采用纯非null值的方法,则可以将其映射到Optional,因此对于输入,纯版本严格来说更加不确定。但是,仍然有很多可能的原因导致您Optional仍然需要一个参数:

  • 您希望将函数与返回一个 Optional
  • Optional如果给定值为空,则函数应返回非空值
  • 您认为Optional真棒,应该要求使用您的API的人来了解它;-)

10

用的模式Optional是为了避免返回 null。仍然完全可以传递null给方法。

尽管这些还不是真正官方的,但是您可以使用JSR-308样式注释来指示您是否接受null该函数中的值。请注意,您必须具有正确的工具来实际识别它,并且它提供的静态检查多于可执行的运行时策略,但这会有所帮助。

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

这里的问题是@NotNull不是默认情况,必须显式注释-因此,由于懒惰,人们不会做的样板工作。
dwegener '16

1
@dwegener是的,但是Optional也不能解决问题,因此我建议使用注释。
Makoto 2016年

2
忽略Optional比忽略注释要困难得多,后者是仅在阅读文档/源代码或您的工具可以捕获注释时才会注意到的注释(静态分析不能始终正确确定可空性)。
Ajax

请注意,@ Nullable可能并不意味着您“接受” null作为有效值,即不引发任何异常。这可能只是意味着您要防范这种情况,但仍会引发异常(这是Findbugs的语义)。因此,您可以在此类注释中引入歧义而不是清楚。
tkruse17年

我不确定@ user2986984有什么歧义。“不处理null”只能现实地表示抛出异常。
Makoto

7

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

就是说,将采用/返回可选参数的方法链接在一起是一件很合理的事情,例如Maybe monad。


7
返回值和字段也不能为null吗?那么Optional完全没有意义吗?
塞缪尔·埃德温·沃德

6

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

API注意:可选方法主要用于可选方法返回类型,在这种情况下,很明显需要表示“无结果”,并且使用null可能会导致错误。


4

我认为Optional应该是Monad,而在Java中则无法想象。

在函数式编程中,您处理的是纯净和高阶函数,这些函数仅根据其“业务域类型”来接受和组合其参数。组成以现实世界为基础或应向其报告计算结果的函数(所谓的副作用)要求应用函数,这些函数负责自动从代表外部世界的单子中分解出值(状态,配置,期货,也许,无论是哪种,作家等);这称为提升。您可以将其视为关注点分离。

混合这两个抽象级别并不能提高可读性,因此最好避免使用它。


3

传递Optionalas参数时要小心的另一个原因是,方法应该做一件事...如果传递Optional参数,则您可能希望做不止一件事情,传递布尔参数可能类似于。

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

我认为这不是一个好理由。当使用非可选参数时,可以将相同的逻辑应用于检查param是否为null。实际上,if(param.isPresent())使用Optional不是最好的方法,相反,您可以使用:param.forEach(() -> {...})
hdkrus

我也不喜欢传递可为空的参数的想法。无论如何,我说您可能会赞成,当然,您可能会使用它,这取决于您,请谨慎使用,仅此而已。没有适用于所有场景的规则。
波城

2

接受Optional作为参数会导致在调用者级别进行不必要的包装。

例如,在以下情况下:

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

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

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

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

当您必须与其他“可映射”结构(例如,)组合时,情况甚至更糟。可以

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为参数,这几乎总是一种代码气味,表明从调用方到被调用方的控制流泄漏,调用方应负责检查Option的内容

参考 https://github.com/teamdigitale/digital-citizenship-functions/pull/148#discussion_r170862749


1

我认为这是因为您通常编写函数来处理数据,然后将其提升为Optional使用map和类似的函数。这将Optional为其添加默认行为。当然,在某些情况下,有必要编写自己的辅助函数即可Optional


1

我相信,存在的共鸣是您必须首先检查Optional是否为null,然后尝试评估它包装的值。太多不必要的验证。


2
传递空的Optional引用可以被认为是编程错误(因为Optional提供了传递“空”值的显式方式),因此不值得检查,除非您希望避免在所有情况下都引发异常。
pvgoran '16

1

布莱恩·格茨Brian Goetz)很好地解释了,并非为此目的设计了可选项。

您始终可以使用@Nullable表示方法参数可以为null。使用可选方法并不能真正使您更整齐地编写方法逻辑。


5
抱歉,我不同意“未设计”或“有人反对”的说法。工程师,我们应该具体。使用@Nullable比使用Optional更糟糕,因为从API角度来看,Optionals更为冗长。我看不出有任何很好的理由反对使用Optionals作为参数(前提是您不想使用方法重载)
Kacper86,2017年

0

另一种方法,您可以做的是

// 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很难看。


0

起初,我也更喜欢将Optionals作为参数传递,但是如果从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();

0

我知道这个问题更多地是关于观点而不是事实。但是我最近从成为.net开发人员转为使用Java开发人员,因此我只是最近才加入Optional方。另外,我更愿意将此表示为评论,但由于我的观点不允许我发表评论,因此我不得不将此作为答案。

根据我的经验,我一直在做的事情对我很有帮助。是将Optionals用于返回类型,并且如果我需要Optional的值以及天气是否在方法中Optional都有值,则仅将Optionals用作参数。

如果我只关心该值,则在调用该方法之前检查isPresent,如果我在该方法中有某种取决于该值是否存在的日志记录或其他逻辑,那么我将很高兴地传递Optional。


0

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

开发人员负责提供精确的规范和正确的实现。因此,如果开发人员已经知道参数是可选的,则实现必须正确处理它,无论它是null还是Optional。API对用户应该尽可能简单,而null是最简单的。

另一方面,结果从API开发人员传递给用户。但是,规范是完整而冗长的,用户仍然有可能不知道它,或者只是懒惰地对其进行处理。在这种情况下,“可选”结果会强制用户编写一些额外的代码来处理可能为空的结果。


0

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

int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));

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

public int calculateSomething(String p1OrNull, BigDecimal p2OrNull)

这样,很明显允许空值。

您对的使用p1.orElse(null)说明了使用Optional时代码的详细程度,这就是我避免使用它的部分原因。可选是为函数编程编写的。流需要它。您的方法可能永远不要返回Optional,除非在函数式编程中必须使用它们。有些方法(例如Optional.flatMap()method)需要引用返回Optional的函数。这是它的签名:

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)

因此,通常这是编写返回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));
}

因此,假设您有一个这样的吸气剂: String getName()

您不能像这样将其传递给flatMap:

opt.flatMap(Widget::getName) // Won't work!

但是您可以这样传递它:

opt.flatMap(optFun(Widget::getName)) // Works great!

在函数式编程之外,应避免使用Optional。

布莱恩·格茨(Brian Goetz)说的最好:

向Java添加Optional的原因是:

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;

1
嗯,这就是它被添加到Java中的原因之一。还有很多其他的。我个人最喜欢用单个链接调用Optional.map的序列替换长链嵌套的“ if”语句,这些语句遍历到数据结构中。除了简单地替换空检查之外,函数式语言编程世界还有很多用于Optional的有趣用法。
盖伊(Guy Guy)
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.