为什么Java 8的Optional不能用于参数

时间:2015-08-10 14:58:00

标签: java java-8 optional

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

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

许多网页指定Optional不应该用作方法参数。考虑到这一点,我可以使用以下方法签名并添加一个清晰的Javadoc注释来指定参数可能为null,希望将来的维护者将读取Javadoc,因此在使用参数之前始终执行空值检查(解决方案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
}

现在我尝试编写类的代码,为每种方法调用这条逻辑。我首先从另一个返回Optional s的对象中检索两个输入参数,然后调用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();
    }
}

所以我的问题是:为什么将Optional用作方法参数(参见解决方案1)被认为是不好的做法?对我来说它看起来是最易读的解决方案最明显的是,参数对于未来的维护者来说可能是空的/ null。 (我知道Optional的设计者打算只将它用作返回类型,但我找不到任何逻辑上的理由不在这种情况下使用它。)

22 个答案:

答案 0 :(得分:149)

哦,那些编码风格应该带点盐。

  1. (+)将Optional结果传递给另一个方法,不进行任何语义分析;把它留给方法,很好。
  2. ( - )使用在方法中导致条件逻辑的可选参数实际上是反作用的。
  3. ( - )需要在Optional中打包一个参数,对于编译器来说是次优的,并且会进行不必要的包装。
  4. ( - )与可空参数相比,可选性更高。
  5. 一般情况下:可选统一两个状态,必须解开。因此,对于数据流的复杂性,更适合结果而不是输入。

答案 1 :(得分:132)

我在这个主题上看到的best postDaniel 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));
     

而不是提供清晰度,可选的工厂方法   上课只会分散读者的注意力。请注意,只有一个可选项   参数,但想象有两个或三个。鲍勃叔叔肯定   不会为这样的代码感到自豪

     

当一个方法可以接受可选参数时,最好采用经过充分验证的方法并使用方法设计这种情况   重载。在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);

答案 2 :(得分:69)

几乎没有充分理由不使用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 :(得分:10)

这个建议是&#34;的变体,尽可能不是关于输入的,并且尽可能具体地关于输出&#34;经验法则。

通常,如果您有一个采用普通非空值的方法,则可以将其映射到Optional,因此普通版本对输入严格更加不明确。 然而仍然存在许多可能的原因,为什么您仍然需要Optional参数:

  • 您希望您的函数与另一个返回Optional
  • 的API一起使用
  • 如果给定值为空,您的函数应返回空Optional以外的其他内容
  • 您认为Optional非常棒,无论谁使用您的API都需要了解它; - )

答案 4 :(得分:8)

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

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

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

答案 5 :(得分:6)

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

话虽如此,将接受/返回选项的方法链接在一起是合理的事情,例如,也许monad。

答案 6 :(得分:3)

我认为Optional应该是Monad,而这些在Java中是不可想象的。

在函数式编程中,您处理纯函数和高阶函数,这些函数仅根据其“业务域类型”来获取和组合它们的参数。编写以现实世界(所谓的副作用)为基础或计算应该报告的函数需要应用函数来处理从表示外部世界的monad中自动解包值(状态,配置,期货,也许,要么,作家等......);这称为提升。您可以将其视为一种关注点分离。

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

答案 7 :(得分:2)

让我们弄清楚一点:在其他语言中,没有普遍建议不要将Maybe类型用作字段类型,构造函数参数类型,方法参数类型或函数参数类型。

因此,如果您“不应”在Java中使用Optional作为参数类型,则原因是特定于,针对Java或两者兼有。

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

Brian Goetz

[W] e确实很清楚 添加[可选]时的意图,而不是一般性的 目的也许有很多人会喜欢我们做的 所以。我们的目的是为图书馆提供有限的机制 需要明确表示方式的方法返回类型 “没有结果”,并且对此使用null绝对有可能 导致错误。

例如,您可能永远不应该将其用于 返回结果数组或结果列表;而是返回一个 空数组或列表。您几乎永远不应将其用作 东西或方法参数。

因此答案是特定于可选的:它不是“通用Maybe类型”;因此,它受到限制,并且可能以限制其用作字段类型或参数类型的用途的方式受到限制。

也就是说,在实践中,我很少发现使用Optional作为字段类型或参数类型会引起问题。如果Optional尽管有其局限性,但可以用作您的用例的参数类型或字段类型,请使用它。

答案 8 :(得分:2)

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

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

答案 9 :(得分:1)

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

答案 10 :(得分:1)

在JDK10 {{3}}中签出JavaDoc,添加了一个API注释:

  

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

答案 11 :(得分:1)

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

例如在以下情况下:

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

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

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

即使你知道它们不是空的,你也被迫将它们包装在Optional中。

当你必须与其他&#34; mappable&#34;组合时,情况会变得更糟。结构,即。 Eithers

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

REF:

  

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

REF。 https://github.com/teamdigitale/digital-citizenship-functions/pull/148#discussion_r170862749

答案 12 :(得分:1)

选项不是为此目的而设计的,Brian Goetz很好地解释了这一点。

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

答案 13 :(得分:1)

我认为这是因为您经常编写函数来操作数据,然后使用Optional和类似函数将其提升到map。这会向其添加默认的Optional行为。 当然,可能会出现需要编写适用于Optional的辅助功能的情况。

答案 14 :(得分: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();

答案 15 :(得分:0)

我知道这个问题更多的是关于意见而不是事实。但是我最近从一个.net开发者变成了一个java开发者,所以我最近才加入了Optional party。 另外,我更愿意将此作为评论,但由于我的观点水平不允许我发表评论,我不得不将其作为答案。

我一直在做的事情,这对我来说是一个经验法则。是使用Optionals作为返回类型,只使用Optionals作为参数,如果我同时需要Optional的值,那么天气与否则Optional在方法中有一个值。

如果我只关心这个值,我在调用方法之前检查isPresent,如果我在方法中有某种记录或不同的逻辑,这取决于值是否存在,那么我很乐意传入Optional。 / p>

答案 16 :(得分: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难看。

答案 17 :(得分:0)

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

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

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

答案 18 :(得分: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()方法)需要引用返回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)说了最好的话:

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

答案 19 :(得分:0)

尽管这个话题很旧,但我现在对此很感兴趣。

就我个人而言,我更喜欢解决方案3,同时向方法提供参数始终被视为非null(我喜欢添加lomboks @NonNull)。因此,如果您真的想接受所有4种情况,为什么不通过参数对象呢?也许您的 otherObject 可以满足?总的来说,我会问我是否真的想提供一个接受那些(可选)值的API,还是可以重新构造以避免使用可选参数?如果您认为确实需要可选参数,则认为您违反了SRP。

答案 20 :(得分:0)

也许我会激起一堆低票和负面评论,但是...我不能忍受。

免责声明:我在下面写的内容实际上并不是对原始问题的答案,而是我对该主题的看法。而唯一的来源就是我的想法和经验(使用Java和其他语言)。

首先让我们检查一下,为什么有人要使用Optional?

对我来说,原因很简单:与其他语言不同,java不具有将变量(或类型)定义为可为空或不为空的内置功能。所有“对象”变量都是可为空的,而所有基本类型则不是。为了简单起见,在以后的讨论中不要考虑原始类型,因此我将简单地声明所有变量都是可为空的。

为什么需要将变量声明为可为空/不可为空?好吧,对我来说,原因是:显式总是比隐式更好。除了具有显式修饰(例如注释或类型)之外,还可以帮助静态分析器(或编译器)捕获一些与空指针相关的问题。

许多人在上面的评论中指出,函数不需要具有可为空的参数。而是应使用重载。但是这样的表述仅在教科书中是好的。在现实生活中有不同的情况。考虑类,它表示某些系统的设置,或者某些用户的个人数据,或者实际上是任何复合数据结构,其中包含许多字段-许多字段具有重复类型,并且某些字段是必填字段,而其他字段是可选字段。在这种情况下,继承/构造函数重载实际上并没有帮助。

随机示例:假设,我们需要收集有关人的数据。但是有些人不想提供所有数据。当然是POD,因此基本上是带有值语义的类型,所以我希望它或多或少是不可变的(无设置器)。

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标准,原始类型除外)。所以我们检查了所有这些。

结果:代码会随着检查而爆炸,这几乎是不需要的,因为如上所述,几乎所有时间我们都可以使用可为空的变量,并且仅在少数情况下才需要“可为空”。

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

结果:它实际上不起作用。人们懒于编写和阅读文档。除了最近的趋势是,我们应该避免编写文档以利于使代码本身能够自我描述。除了修改代码和忘记修改文档的所有推理仍然有效。

方法3:@Nullable @NonNull等...我个人认为它们很好。但是存在某些缺点:(例如,它们仅受外部工具而非编译器的尊重),最糟糕的是它们不是标准的,这意味着1.我需要在项目中添加外部依赖项才能受益2.他们被不同系统对待的方式并不统一。据我所知,它们被投票否决了Java的官方标准(我不知道是否有任何重试计划)。

方法4:可选<>。其他评论中已经提到了这些缺点,其中最严重的是(IMO)性能损失。即使我个人发现,它也增加了一些样板,使用Optional.empty()和Optional.of()并不是很糟糕。优势显而易见:

  1. 它是Java标准的一部分。
  2. 对于代码阅读者(或API用户)而言,很明显,这些参数可以为null。此外,它通过显式包装/展开值来强制API的用户和方法的开发者(通过使用@Nullable等注解,情况并非如此)来避免这一事实。

因此,就我而言,包括该方法在内的任何方法都不存在黑白问题。我个人最终得到以下指导原则和约定(仍然不是严格的规则):

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

仍有灰色区域,这些约定不起作用:

  • 我们需要高性能(例如,处理大量数据,以便总执行时间非常长,或者在吞吐量至关重要的情况下)。在这种情况下,由Optional引入的性能损失可能确实是不必要的。
  • 我们处于自己编写的代码的边界,例如:从数据库,Rest Endpoint,解析文件等中读取
  • 或者我们只使用一些外部库,这些库不遵循我们的约定,因此再次,我们应该小心...

顺便说一句,最后两种情况也可以是可选字段/参数中的需求来源。即当数据的结构不是我们自己开发的,而是由某些外部接口,db-schemas等施加的。

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

答案 21 :(得分:0)

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

public void setParameters(Optional<A> op1, Optional<B> op2) {
    ProtoRequest.Builder builder = ProtoRequest.newBuilder();
    op1.ifPresent(builder::setOp1);
    op2.ifPresent(builder::setOp2);
...
}

我认为在这种情况下,将可选参数作为参数可能会很有用。接收 proto 请求的 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);
    }
...
}