Java 8 getter应该返回可选类型吗?

在Java 8中引入的Optional类型对许多开发人员来说是一个新事物。

一个返回Optional<Foo>类型的getter方法代替经典的Foo是一个好的实践吗?假设该值可以是null

87294 次浏览

我想说,一般来说,对于返回值可以为空的可选类型是个好主意。然而,对于框架,我认为用可选类型替换经典的getter方法会在使用依赖于getter和setter编码约定的框架(例如Hibernate)时带来很多麻烦。

在我自己做了一些研究之后,我发现了一些事情,可以建议什么时候这样做合适。最权威的是以下引用自Oracle文章:

__abc0 - __abc1

我还从Java 8可选:如何使用中找到了这个摘录

“Optional并不适用于这种情况,因为它并不能为我们买到任何东西:

  • 在领域模型层(不可序列化)
  • 在dto中(同样的原因)
  • 输入方法的参数
  • 在构造函数参数中

这似乎也提出了一些有效的观点。

我没有找到任何负面含义或危险信号来建议Optional应该避免。我认为一般的想法是,如果它有帮助或提高了API的可用性,就使用它。

当然,人们会做他们想做的事。但是在添加这个特性时,我们确实有一个明确的意图,并且是一个通用的Maybe类型,就像很多人希望我们这样做一样。我们的意图是为库方法返回类型提供一种有限的机制,其中需要一种明确的方式来表示“没有结果”,而使用null来表示这种类型极有可能导致错误。

例如,你可能永远不应该将它用于返回结果数组或结果列表的事情;而是返回一个空数组或列表。几乎不应该将它用作某个字段或方法参数。

我认为常规地使用它作为getter的返回值肯定是过度使用的。

在Optional中没有任何错误的是应该避免的,它只是不是许多人所希望的那样,因此我们相当担心过度使用的风险。

(公共服务公告:从来没有调用Optional.get,除非你能证明它永远不会为空;而是使用安全的方法,如orElseifPresent。回想起来,我们应该将get称为类似getOrElseThrowNoSuchElementException的东西,或者更清楚地表明这是一个非常危险的方法,它首先破坏了Optional的整个目的。教训。(更新:Java 10有Optional.orElseThrow(),它在语义上等价于get(),但它的名字更合适。))

如果你正在使用现代序列化器和其他理解Optional的框架,那么我发现这些的指导方针在编写Entity bean和域层时工作得很好:

  1. 如果序列化层(通常是DB)允许表FOOBAR列中的单元格有null值,那么getter Foo.getBar()可以返回Optional,向开发人员表明这个值可以合理地预期为空,他们应该处理这个值。如果DB保证该值不会为空,则getter应该将其包装在Optional中。
  2. Foo.bar应该是private应该是Optional。如果它是private,就没有理由为Optional
  3. setter Foo.setBar(String bar)应该采用bar Optional的类型。如果可以使用null参数,则在JavaDoc注释中声明此参数。如果不能使用null,则使用IllegalArgumentException或一些适当的业务逻辑更合适。
  4. 构造函数不需要Optional参数(原因与第3点类似)。通常我只在构造函数中包含序列化数据库中必须非空的参数。

为了使上面的方法更有效,你可能想要编辑你的IDE模板来生成getter和对应的toString()equals(Obj o)等模板,或者直接使用这些字段(大多数IDE生成器已经处理空值)。

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;

我的观点是Optional是为支持函数式编程而编写的,它同时被添加到Java中。(这个例子来自Brian Goetz的博客。一个更好的例子可能是使用orElse()方法,因为这段代码无论如何都会抛出异常,但你可以理解。)

但现在,人们使用Optional的原因截然不同。他们用它来解决语言设计中的一个缺陷。缺陷在于:没有办法指定API的哪些参数和返回值允许为空。它可能会在javadocs中提到,但大多数开发人员甚至不会为他们的代码编写javadocs,也没有多少人会在编写时检查javadocs。因此,这导致许多代码在使用空值之前总是检查它们,即使它们通常不可能为空,因为它们已经在调用堆栈中重复验证了9或10次。

我认为人们确实渴望解决这个缺陷,因为很多人看到新的Optional类后都认为它的目的是为api增加清晰度。这就是为什么人们会问这样的问题:“getter应该返回optional吗?”不,它们可能不应该,除非您希望getter用于函数式编程,但这是不可能的。事实上,如果您查看Java API中使用Optional的地方,就会发现它主要在Stream类中,这是函数式编程的核心。(我没有检查得很彻底,但Stream类可能是它们使用的只有位置。)

如果您确实计划在一些函数代码中使用getter,那么使用一个标准getter和返回Optional的第二个getter可能是个好主意。

哦,如果你需要你的类是可序列化的,你绝对不应该使用Optional。

对于API缺陷来说,可选选项是一个非常糟糕的解决方案,因为a)它们非常冗长,b)它们从一开始就没有打算解决这个问题。

对API缺陷更好的解决方案是Nullness检查程序。这是一个注释处理器,允许您通过使用@Nullable注释指定哪些参数和返回值允许为空。通过这种方式,编译器可以扫描代码,并确定一个实际上可以为null的值是否被传递给了一个不允许为null的值。默认情况下,它假定任何东西都不允许为空,除非它被注释为空。这样,您就不必担心空值了。将空值传递给参数将导致编译器错误。测试一个不可能为空的对象是否为空会产生编译器警告。这样做的效果是将NullPointerException从运行时错误更改为编译时错误。

这改变了一切。

至于你的getter,不要使用Optional。试着设计你的类,让所有成员都不可能是空的。也许可以尝试添加Nullness检查器到你的项目,并声明你的getter和setter参数@Nullable如果他们需要它。我只在新项目中这样做过。它可能会在现有的项目中产生大量的警告,其中包含大量多余的null测试,因此可能很难进行改进。但它也会捕获很多bug。我很喜欢。因此,我的代码更加清晰可靠。

(还有一种新的语言可以解决这个问题。Kotlin可以编译为Java字节代码,它允许您在声明对象时指定对象是否可以为空。这是一种更干净的方法。)

原文补遗(第二版)

经过一番思考后,我勉强得出结论:在一个条件下返回Optional是可以接受的:检索到的值实际上可能是null。我见过很多代码,人们例行地从getter返回Optional,而这些getter不可能返回null。我认为这是一种非常糟糕的编码实践,只会增加代码的复杂性,从而更容易产生错误。但是当返回值实际上可能为空时,请继续并将其包装在Optional中。

请记住,为函数式编程设计的方法,以及需要函数引用的方法,将(并且应该)以两种形式编写,其中一种使用Optional。例如,Optional.map()Optional.flatMap()都接受函数引用。第一个参数接受一个对普通getter的引用,第二个参数接受一个返回Optional的getter。因此,返回值不能为null的Optional并不是对任何人都有帮助。

说了这么多,我仍然认为Nullness检查程序使用的方法是处理空值的最佳方法,因为它们将nullpointerexception从运行时错误转变为编译时错误。

您必须记住,经常被引用的建议来自那些在Java之外几乎没有任何经验的人,他们没有选项类型或函数式编程经验。

所以你要半信半疑。相反,让我们从“良好实践”的角度来看待它;角度:

良好的实践不仅意味着要问“我们如何编写新代码”,而且还要问“现有的代码会发生什么”。

Optional的情况下,我的环境找到了一个很好的和容易遵循的答案:

Optional是强制性的,用于指示records中的可选值:

record Pet(String name, Optional<Breed> breed,
Optional<ZonedDateTime> dateOfBirth)

这意味着现有的代码是很好的,但是使用record的代码(即“新代码”)会导致围绕它的Optional被广泛采用。

结果在可读性和可靠性方面取得了圆满的成功。只要停止使用null即可。