我在许多网站上阅读过 Optional 应该只用作返回类型,而不是在方法参数中使用。我正在努力寻找一个合乎逻辑的原因。例如,我有一段逻辑,它有 2 个可选参数。因此,我认为像这样编写我的方法签名是有意义的(解决方案 1):
public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {
// my logic
}
许多网页指定 Optional 不应用作方法参数。考虑到这一点,我可以使用以下方法签名并添加明确的 Javadoc 注释来指定参数可能为空,希望未来的维护人员会阅读 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
的对象中检索两个输入参数,然后调用 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
s 作为方法参数被认为是不好的做法(请参阅解决方案 1)? 这对我来说似乎是最易读的解决方案,并且最明显的是参数对未来的维护者来说可能是空的/null。 (我知道 Optional
的设计者打算将其仅用作返回类型,但我找不到在这种情况下不使用它的任何合乎逻辑的理由)。
null
吗?
哦,这些编码风格需要加点盐。
(+) 将 Optional 结果传递给另一个方法,不进行任何语义分析;把它留给方法,很好。 (-) 在方法内部使用导致条件逻辑的可选参数实际上是适得其反的。 (-) 需要在 Optional 中打包参数,对于编译器来说是次优的,并且会进行不必要的包装。 (-) 与可为空的参数相比,Optional 的成本更高。 (-) 有人在实际参数中将 Optional 作为 null 传递的风险。
一般来说: Optional 统一了两个必须解开的状态。因此,对于数据流的复杂性,结果比输入更适合。
我在该主题上看到的 best post 是由 Daniel Olszewski 编写的:
尽管对于非强制性方法参数考虑 Optional 可能很诱人,但与其他可能的替代方案相比,这种解决方案显得苍白无力。为了说明问题,请检查以下构造函数声明: public SystemMessage(String title, String content, Optional
null
或 { 3}。
几乎没有充分的理由不使用 Optional
作为参数。反对这一点的论点依赖于权威的论点(参见 Brian Goetz - 他的论点是我们不能强制执行非 null 选项)或者 Optional
参数可能为 null(本质上是相同的参数)。当然,Java 中的任何引用都可以为空,我们需要鼓励编译器强制执行规则,而不是程序员的内存(这是有问题的并且无法扩展)。
函数式编程语言鼓励使用 Optional
参数。使用它的最佳方法之一是拥有多个可选参数并使用 liftM2
来使用函数,假设参数不为空并返回可选参数(请参阅 http://www.functionaljava.org/javadoc/4.4/functionaljava/fj/data/Option.html#liftM2-fj.F-)。不幸的是,Java 8 实现了一个非常有限的支持可选的库。
作为 Java 程序员,我们应该只使用 null 与遗留库进行交互。
@Nonnull
和 @Nullable
怎么样?我在自己开发的库中大量使用它们,IDE(IntelliJ)提供的支持非常好。
@NonNull
注释方法参数是完全可以的,如果它们以不同的方式服务于相同的目的? "对我来说,只有一个论点至少具有某种意义:“应该使用 Optional 来提供具有明确返回类型的更好的 API。但是对于方法参数,可以使用重载函数。” - 不过,我不认为这是一个足够有力的论点。
让我们明确一点:在其他语言中,没有一般建议反对将 Maybe 类型用作字段类型、构造函数参数类型、方法参数类型或函数参数类型。
因此,如果您“不应该”在 Java 中将 Optional 用作参数类型,则原因特定于 Optional、Java 或两者。
可能适用于其他 Maybe 类型或其他语言的推理在这里可能无效。
[W] 在添加 [Optional] 时确实有明确的意图,它不是通用的 Maybe 类型,就像很多人希望我们这样做一样。我们的目的是为库方法返回类型提供一种有限的机制,其中需要一种清晰的方式来表示“无结果”,并且使用 null 来表示这种方法极有可能导致错误。例如,您可能永远不应该将它用于返回结果数组或结果列表的东西;而是返回一个空数组或列表。您几乎不应该将它用作某物的字段或方法参数。
所以答案是针对 Optional 的:它不是“通用的 Maybe 类型”;因此,它是有限的,并且可能以限制其作为字段类型或参数类型的有用性的方式受到限制。
也就是说,在实践中,我很少发现使用 Optional 作为字段类型或参数类型是一个问题。如果 Optional 尽管有其限制,但仍可用作您的用例的参数类型或字段类型,请使用它。
带有 Optional
的模式是为了避免返回 null
。仍然完全可以将 null
传递给方法。
虽然这些还不是正式的,但您可以使用 JSR-308 style 注释来指示您是否接受 null
值到函数中。请注意,您必须拥有正确的工具才能实际识别它,并且它提供的静态检查比可执行的运行时策略更多,但它会有所帮助。
public int calculateSomething(@NotNull final String p1, @NotNull final String p2) {}
Optional
也不能解决问题,因此我建议使用注释。
该建议是“对输入尽可能不具体,对输出尽可能具体”经验法则的变体。
通常,如果您有一个采用普通非空值的方法,则可以将其映射到 Optional
,因此纯版本在输入方面严格来说更加不具体。 然而仍然有很多可能的原因需要您需要 Optional
参数:
您希望您的函数与另一个返回 Optional 的 API 结合使用
如果给定值为空,则您的函数应返回空 Optional 以外的其他内容
您认为 Optional 太棒了,以至于任何使用您的 API 的人都应该被要求了解它;-)
查看JDK10中的JavaDoc,https://docs.oracle.com/javase/10/docs/api/java/util/Optional.html,添加了一个API注释:
API 注意: Optional 主要用作方法返回类型,其中明确需要表示“无结果”,并且使用 null 可能会导致错误。
这对我来说似乎有点傻,但我能想到的唯一原因是方法参数中的对象参数在某种程度上已经是可选的——它们可以为空。因此,强迫某人获取现有对象并将其包装在可选项中是毫无意义的。
话虽如此,将获取/返回选项的方法链接在一起是一件合理的事情,例如Maybe monad。
Optional
完全没有意义?
接受 Optional 作为参数会导致在调用者级别进行不必要的包装。
例如在以下情况下:
public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {}
假设您有两个非空字符串(即从其他方法返回):
String p1 = "p1";
String p2 = "p2";
即使您知道它们不是空的,您也不得不将它们包装在 Optional 中。
当您必须与其他“可映射”结构组合时,情况会变得更糟,即。 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 >));
参考:
方法不应该期望 Option 作为参数,这几乎总是一种代码味道,表明从调用者到被调用者的控制流泄漏,调用者应该负责检查选项的内容
参考。 https://github.com/teamdigitale/digital-citizenship-functions/pull/148#discussion_r170862749
我的看法是 Optional 应该是 Monad,而这些在 Java 中是不可想象的。
在函数式编程中,您处理纯函数和高阶函数,这些函数仅基于其“业务领域类型”来获取和组合它们的参数。组成以现实世界为基础或应向其报告计算的函数(所谓的副作用)需要应用一些函数,这些函数负责自动将值从代表外部世界的单子中解包出来(状态、配置、 Futures、Maybe、Either、Writer 等……);这称为提升。您可以将其视为一种关注点分离。
混合这两个抽象级别不利于易读性,所以你最好避免它。
也许我会招来一堆反对票和负面评论,但是……我受不了。
免责声明:我在下面写的并不是对原始问题的真正答案,而是我对该主题的看法。它的唯一来源是我的想法和我的经验(使用 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() 并没有那么糟糕。优点很明显:
它是 Java 标准的一部分。代码的读者(或 API 的用户)很明显,这些参数可能为空。此外,它还强制:API 的用户和方法的开发人员通过显式包装/解包值来了解这一事实(当使用 @Nullable 等注释时,情况并非如此)。
因此,在我看来,包括这一方法在内的任何方法都没有非黑即白之分。我个人最终得到了以下准则和约定(仍然不是严格的规则):
在我自己的代码中,所有变量都必须不为空(但可能是 Optional<>)。如果我有一个带有一个或两个可选参数的方法,我会尝试使用重载、继承等重新设计它。如果我无法在合理的时间内找到解决方案,我会开始思考,如果性能很关键(即如果有数百万个对象待处理)。通常情况并非如此。如果没有,我使用 Optional 作为参数类型和/或字段类型。
仍然存在灰色区域,这些约定不起作用:
我们需要高性能(例如处理大量数据,因此总执行时间非常长,或者吞吐量很关键的情况)。在这种情况下,由 Optional 引入的性能损失可能真的是不需要的。
我们在自己编写的代码的边界上,例如:我们从 DB、Rest Endpoint、解析文件等读取。
或者我们只是使用一些不符合我们约定的外部库,所以再次强调,我们应该小心......
顺便说一句,最后两种情况也可能是可选字段/参数中的需求来源。即当数据的结构不是我们自己开发的,而是由一些外部接口、db-schemas等强加的时候......
最后,我认为,应该考虑正在解决的问题,并尝试找到合适的工具。如果 Optional<> 是合适的,那么我认为没有理由不使用它。
编辑:方法 5:我最近使用了这个,当时我无法使用 Optional
。这个想法只是对方法参数和类变量使用命名约定。我使用了“maybe”前缀,这样如果“url”参数可以为空,那么它就变成了maybeUrl
。优点是它略微提高了意图的可理解性(并且没有其他方法的缺点,例如外部依赖性或性能损失)。但也有缺点,例如:没有工具支持这种约定(如果您在没有先检查的情况下访问“可能”变量,您的 IDE 不会向您显示任何警告)。另一个问题是,只有在所有从事该项目的人一致应用时,它才会有所帮助。
OrNull
,例如 getUrlOrNull()
,用户很清楚此方法可能会返回 null。
传递 Optional
作为参数时要小心的另一个原因是方法应该做一件事......如果传递 Optional
参数,您可能倾向于做不止一件事,它可能类似于传递布尔参数.
public void method(Optional<MyClass> param) {
if(param.isPresent()) {
//do something
} else {
//do some other
}
}
if(param.isPresent())
不是使用 Optional 的最佳方法,您可以使用:param.forEach(() -> {...})
我认为这是因为您通常编写函数来操作数据,然后使用 map
和类似函数将其提升到 Optional
。这会为其添加默认的 Optional
行为。当然,在某些情况下,您可能需要编写自己的辅助函数以在 Optional
上运行。
我相信存在的原因是您必须首先检查 Optional 本身是否为空,然后尝试评估它包装的值。太多不必要的验证。
我知道这个问题更多的是关于意见而不是确凿的事实。但我最近从 .net 开发人员转到了 java 开发人员,所以我最近才加入了 Optional 派对。另外,我更愿意将此声明为评论,但由于我的积分水平不允许我发表评论,因此我不得不将其作为答案。
我一直在做的事情,作为一个经验法则,这对我很有帮助。是使用 Optionals 作为返回类型,并且只使用 Optionals 作为参数,如果我需要 Optional 的值,并且天气或 Optional 在方法中没有值。
如果我只关心值,我会在调用方法之前检查 isPresent,如果我在方法中有某种日志记录或不同的逻辑,取决于值是否存在,那么我会很乐意传入 Optional。
在某些涉及 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);
}
...
}
正如 Brian Goetz 很好地解释的那样,可选项不是为此目的而设计的。
您始终可以使用 @Nullable 来表示方法参数可以为空。使用可选项并不能真正让您更整洁地编写方法逻辑。
另一种方法,你可以做的是
// 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 变得丑陋。
起初,我也更喜欢将 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();
这是因为我们对 API 用户和 API 开发人员有不同的要求。
开发人员负责提供精确的规范和正确的实现。因此,如果开发人员已经知道参数是可选的,则实现必须正确处理它,无论它是 null 还是 Optional。 API 对用户来说应该尽可能简单,而 null 是最简单的。
另一方面,结果从 API 开发人员传递给用户。然而,规范是完整而冗长的,用户仍然有可能不知道它或者只是懒于处理它。在这种情况下, Optional 结果迫使用户编写一些额外的代码来处理可能的空结果。
首先,如果您使用的是方法 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。有些方法(例如 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;
不管 Java 8,使用老式方法重载技术来带来清晰和灵活性,假设您有以下带有两个 args 的方法
public void doSomething(arg1,arg2);
如果你想添加额外的可选参数然后重载方法
public void doSomething(arg1,arg2,arg3) {
Result result = doSomething(arg1,arg2);
// do additional working
}
一个很好的例子是可选的,因为参数会很好是 JPA 存储库。我喜欢做 findByNameAndSurname(Optional,Optional) 之类的事情。这样,如果 Optional 为空,则不会执行 WHERE param=y
不定期副业成功案例分享
Optional
参数表示三种状态之一:null
可选、带有isPresent() == false
的非空Optional
和包装实际值的非空Optional
。@NotNull Optional
。Optional
,则状态 1(null
可选)通常表示编程错误,因此您最好不要处理它(让它抛出NullPointerException
)