为什么在Java 8+中使用Optional而不是传统的空指针检查?


110

我们最近已经迁移到Java8。现在,我看到应用程序充斥着Optional对象。

Java 8之前(样式1)

Employee employee = employeeServive.getEmployee();

if(employee!=null){
    System.out.println(employee.getId());
}

Java 8之后(样式2)

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employee = employeeOptional.get();
    System.out.println(employee.getId());
}

我看不到Optional<Employee> employeeOptional = employeeService.getEmployee();服务本身返回可选值时的附加值:

来自Java 6背景,我认为样式1更清晰,代码行更少。我在这里没有任何真正的优势吗?

通过对所有答案的理解和博客的进一步研究得到巩固



28
employeeOptional.isPresent()似乎完全缺少选项类型的要点。根据@MikePartridge的评论,不建议这样做或惯用法。明确检查选项类型是某物还是什么都不是禁忌。你应该简单地mapflatMap超过他们。
Andres F.

7
一个堆栈溢出回答-Optional布赖恩戈茨中,Java语言架构师甲骨文。
罗勒·布尔克

6
当您以“最佳做法”的名义关闭大脑时,就会发生这种情况。
immibis

9
那是可选用的相当差的用法,但即使这样做也有好处。随着空一切可能为空,所以你需要不断地进行检查,可选调用这个特殊的变量可能会丢失,请务必检查它
理查德发麻

Answers:


108

样式2不足以使用Java 8来获得全部好处。您根本不想要if ... use。请参阅Oracle的示例。采纳他们的建议,我们得到:

风格3

// Changed EmployeeServive to return an optional, no more nulls!
Optional<Employee> employee = employeeServive.getEmployee();
employee.ifPresent(e -> System.out.println(e.getId()));

或更长的片段

Optional<Employee> employee = employeeServive.getEmployee();
// Sometimes an Employee has forgotten to write an up-to-date timesheet
Optional<Timesheet> timesheet = employee.flatMap(Employee::askForCurrentTimesheet); 
// We don't want to do the heavyweight action of creating a new estimate if it will just be discarded
client.bill(timesheet.orElseGet(EstimatedTimesheet::new));

19
实际上,我们禁止使用isPresent的大多数用法(由代码审查捕获)
jk。

10
@jk。确实,如果出现超载,void ifPresent(Consumer<T>, Runnable)我会主张彻底禁止isPresentget
-Caleth

10
@Caleth:您可能对Java 9感兴趣ifPresentOrElse(Consumer<? super T>, Runnable)
wchargin '18

9
与Java 6相比,我发现这种Java 8风格有一个缺点:它愚弄了代码覆盖工具。使用if语句,它将显示您何时错过分支,而不是在此处。
Thierry

14
@Aaron“大大降低了代码的可读性”。哪一部分完全降低了可读性?听起来更像是“我一直做事都不同,我不想改变自己的习惯”,而不是客观的推理。
Voo

47

如果您Optional用作仍可能返回的较旧API之间的“兼容性”层null,则在确定您拥有某些东西的最新阶段创建(非空)Optional可能会有所帮助。例如,您在何处写道:

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

我会选择:

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

关键是您知道这里有一个非空的员工服务,因此您可以将其包装在Optionalwith中Optional.of()。然后,当您提出要求getEmployee()时,您可能会或可能不会雇用一名雇员。该员工可能有(或可能没有)ID。然后,如果最后得到一个ID,则要打印它。

无需在此代码中显式检查任何 null,状态等。


4
使用(...).ifPresent(aMethod)over有if((...).isPresent()) { aMethod(...); }什么好处?似乎只是另一种语法可以执行几乎相同的操作。
罗斯兰

2
@Ruslan当您已经有一种适用于所有情况的通用解决方案时,为什么要对一个特定的调用使用一种特殊的语法?我们也在这里应用来自map和co的相同想法。
Voo

1
@Ruslan ... API返回null而不是空集合或数组。例如,您可以执行类似的操作(假设Parent类的getChildren()返回一组子级,如果没有子级,则为null):Set<Children> children = Optional.of(parent).map(Parent::getChildren).orElseGet(Collections::emptySet); 现在,我可以children统一处理,例如children.forEach(relative::sendGift);。这些甚至可以合并:Optional.of(parent).map(Parent::getChildren).orElseGet(Collections::emptySet).forEach(relative::sendGift);。空检查等的所有样板,……
约书亚·泰勒

1
@Ruslan ...被委派给标准库中经过验证的真实代码。这减少了代码,我,作为一个程序员,有编写,测试,调试等量
约书亚·泰勒

1
很高兴我正在使用Swift。如果让employee = employeeService.employee {print(employee.Id); }
gnasher729

23

具有Optional单个值的几乎没有附加值。如您所见,这只是用状态检查代替了检查null

拥有这些东西具有巨大的附加价值Collection。引入的所有用于替换旧式低级循环的流方法都可以理解Optional并对其进行正确的处理(不处理它们或最终返回另一个未设置的Optional)。如果您处理n个项目的频率比处​​理单个项目的频率高(所有现实程序中的99%都要处理),那么对可选内容的内置支持是一项巨大的进步。在理想情况下,您不必检查null 存在。


24
除了收藏/流,可选的也很大,使API的更多自我记录,例如,Employee getEmployee()VS Optional<Employee> getEmployee()。尽管从技术上讲两者都可以返回null,但是其中之一更有可能引起麻烦。
阿蒙(Amon)

16
单个值的可选值中有很多值。能够.map()不必检查null一路上是巨大的。例如Optional.of(service).map(Service::getEmployee).map(Employee::getId).ifPresent(this::submitIdForReview);
约书亚·泰勒

9
我不同意您最初的评论没有附加值。可选为您的代码提供上下文。快速浏览可以告诉您正确的功能,并且涵盖所有情况。鉴于使用null检查,是否应该对每个方法的所有单个Reference类型进行空检查?类似的情况可以应用于Class和public / private修饰符;它们会为您的代码添加零值,对吧?
ArTs

3
@JoshuaTaylor老实说,与表达式相比,我更喜欢老式的支票null
Kilian Foth,

2
即使具有单个值的Optional的另一个大优点是,您应该忘记应该检查是否为null。否则,很容易忘记检查并最终导致NullPointerException ...
Sean Burton

17

只要您使用Optional像花哨的API一样isNotNull(),那么是的,仅通过检查就不会发现任何区别null

您不应该使用Optional的东西

使用Optional只是为了检查一个值的存在是错误代码™:

// BAD CODE ™ --  just check getEmployee() != null  
Optional<Employee> employeeOptional =  Optional.ofNullable(employeeService.getEmployee());  
if(employeeOptional.isPresent()) {  
    Employee employee = employeeOptional.get();  
    System.out.println(employee.getId());
}  

您应该使用Optional的东西

通过在使用null返回API方法时在必要时生成一个值,避免出现不存在的值:

Employee employee = Optional.ofNullable(employeeService.getEmployee())
                            .orElseGet(Employee::new);
System.out.println(employee.getId());

或者,如果创建新员工的成本太高:

Optional<Employee> employee = Optional.ofNullable(employeeService.getEmployee());
System.out.println(employee.map(Employee::getId).orElse("No employee found"));

同样,使每个人都知道您的方法可能不会返回值(如果由于某种原因,您无法返回上述默认值):

// Your code without Optional
public Employee getEmployee() {
    return someCondition ? null : someEmployee;
}
// Someone else's code
Employee employee = getEmployee(); // compiler doesn't complain
// employee.get...() -> NPE awaiting to happen, devs criticizing your code

// Your code with Optional
public Optional<Employee> getEmployee() {
    return someCondition ? Optional.empty() : Optional.of(someEmployee);
}
// Someone else's code
Employee employee = getEmployee(); // compiler complains about incompatible types
// Now devs either declare Optional<Employee>, or just do employee = getEmployee().get(), but do so consciously -- not your fault.

最后,所有其他类似流的方法已经在其他答案中进行了解释,尽管并不是真正决定使用的方法,Optional而是利用Optional其他人提供的方法。


1
@Kapep确实。我们在/ r / java进行了辩论,我说«我认为这Optional是一个错失的机会。“在这里,Optional我做了这个新东西,所以您被迫检查空值。哦,顺便说一句,我get()向它添加了一个方法,因此您实际上不必检查任何东西;);)” 。(...)Optional可以很好地用作“ THIS MIGHT BE NULL”霓虹灯,但其get()方法的裸露使它»»。但是OP只是在问使用的原因Optional,而不是反对的原因或设计缺陷。
沃伦

1
Employee employee = Optional.ofNullable(employeeService.getEmployee());在您的第三个代码段中,类型不是Optional<Employee>吗?
查理·哈丁

2
@immibis以什么方式残废?使用的主要原因get是您是否必须与一些旧代码交互。我想不出新代码中有很多有效的理由要使用get。这就是说,与遗留代码进行交互时,通常别无选择,但仍然存在一个问题,即get原因的存在会导致初学者编写不良代码。
Voo

1
@immibis我没有提Map过一次,而且我不知道有人在进行如此激烈的,非向后兼容的更改,因此请确保您可以有意地设计不良的API- Optional不知道能证明什么。一个Optional是有意义的一些tryGet方法,虽然。
Voo

2
@Voo Map是每个人都熟悉的示例。当然,Map.get由于向后兼容性,它们不能使return成为Optional,但是您可以轻松地想象另一个没有此类兼容性约束的类似Map的类。说class UserRepository {Optional<User> getUserDetails(int userID) {...}}。现在,您想要获取登录用户的详细信息,并且知道它们存在,因为它们设法登录并且您的系统从不删除用户。那就这样吧theUserRepository.getUserDetails(loggedInUserID).get()
immibis

12

当开发人员需要检查函数是否返回值时,使用Optional的关键是要清楚。

//service 1
Optional<Employee> employeeOptional = employeeService.getEmployee();
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

//service 2
Employee employee = employeeService.getEmployeeXTPO();
System.out.println(employee.getId());

8
让我感到困惑的是,所有带有可选功能的漂亮功能性东西虽然确实很不错,但在这里比这要重要的多。在Java 8之前,您必须深入研究Javadoc以了解是否必须对调用的响应进行空检查。发布Java 8后,您将从返回类型中知道是否可以“一无所获”。
Buhb

3
好吧,这里有一个“旧代码”问题,事情仍然可能返回null……但是,是的,能够从签名中分辨出来是很棒的。
HaakonLøtveit18年

5
是的,它在其中的可选<T>是表示可能缺少值的唯一方法的语言中肯定会更好,即没有空ptr的语言。
dureuill

8

您的主要错误是您仍在使用更多程序性术语进行思考。这并不意味着对您作为一个人的批评,而仅仅是观察。用更多的功能性思维思考需要时间和实践,因此方法是现成的,看起来像是最明显的正确事物需要您的配合。您的第二个小错误是在方法内部创建Optional。Optional旨在帮助记录某些内容可能会或可能不会返回值的情况。您可能一无所获。

这使您确实编写了看起来清晰合理的完全清晰的代码,但是您被get和isPresent的邪恶双胞胎诱惑所吸引。

当然,问题很快就变成了“为什么存在并到达那里?”

这里很多人想念的一件事是isPresent(),它不是为那些完全掌握有用的lambda的人以及喜欢该功能的人编写的新代码编写的。

但是,它确实为我们带来了几(两个)好处,巨大好处,魅力四射的好处?

  • 它简化了传统代码向新功能的过渡。
  • 它简化了Optional的学习曲线。

第一个很简单。

假设您有一个如下所示的API:

public interface SnickersCounter {
  /** 
   * Provides a proper count of how many snickers have been consumed in total.
   */
  public SnickersCount howManySnickersHaveBeenEaten();

  /**
    * returns the last snickers eaten.<br>
    * If no snickers have been eaten null is returned for contrived reasons.
    */
  public Snickers lastConsumedSnickers();
}

并且您有一个使用此类的旧式类(填入空白):

Snickers lastSnickers = snickersCounter.lastConsumedSnickers();
if(null == lastSnickers) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers);
}

一个人为的例子可以肯定。但是在这里忍受我。

Java 8现在已经启动,我们正努力加入。因此,我们要做的一件事就是我们要用返回Optional的东西替换旧界面。为什么?因为正如其他人已经亲切地提到的那样: 这使人们不必猜测某些内容是否可以为null。 这已经被其他人指出。但是现在我们有一个问题。想象我们有(对不起,当我在一个无辜的方法上按下alt + F7时),在经过良好测试的旧代码中调用此方法的位置有46个,否则它将发挥出色的作用。现在,您必须更新所有这些。

这就是isPresent闪耀的地方。

因为现在:士力架lastSnickers = snickersCounter.lastConsumedSnickers(); if(null == lastSnickers){抛出新的NoSuchSnickersException(); }其他{Consumer.giveDiabetes(lastSnickers); }

变成:

Optional<Snickers> lastSnickers = snickersCounter.lastConsumedSnickers();
if(!lastSnickers.isPresent()) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers.get());
}

这是您可以给新进初级人员的一个简单更改:他可以做一些有用的事情,并且他将同时探索代码库。双赢。毕竟,类似于这种模式的东西非常普遍。现在,您不必重写代码即可使用lambda或其他任何东西。(在这种情况下,这是微不足道的,但是我想起了一些例子,在这些例子中,读者很难做到这一点。)

请注意,这意味着您执行此操作的方法实质上是一种无需处理昂贵的重写即可处理遗留代码的方法。那么新代码呢?

好吧,在您的情况下,您只想打印一些内容,您只需执行以下操作:

snickersCounter.lastConsumedSnickers()。ifPresent(System.out :: println);

这很简单,也很清楚。然后慢慢浮出水面的是,存在get()和isPresent()的用例。它们的存在使您可以机械地修改现有代码以使用较新的类型,而不必考虑太多。因此,您在做的事情在以下方面会被误导:

  • 您正在调用一个可能返回null的方法。正确的想法是该方法返回null。
  • 您使用的是旧的创可贴方法来处理此可选方法,而不是使用包含lambda幻想的美味的新方法。

如果您确实想将Optional用作简单的null安全检查,那么您应该做的就是:

new Optional.ofNullable(employeeServive.getEmployee())
    .map(Employee::getId)
    .ifPresent(System.out::println);

当然,它的漂亮版本如下所示:

employeeService.getEmployee()
    .map(Employee::getId)
    .ifPresent(System.out::println);

顺便说一句,尽管它不是必需的,但我还是建议您在每个操作中使用新行,以便于阅读。一周的任何一天都易于阅读和理解,使节拍变得简洁。

当然,这是一个非常简单的示例,可以很容易地理解我们尝试做的所有事情。在现实生活中并不总是那么简单。但是请注意,在此示例中,我们要表达的是我们的意图。我们想要获取员工,获取其ID,并在可能的情况下进行打印。这是Optional的第二大胜利。它使我们可以创建更清晰的代码。我也确实认为,做一些事情,例如制作一种可以处理很多事情的方法,以便您可以将其提供给地图,通常是个好主意。


1
这里的问题是,您试图使它们听起来像这些功能版本与旧版本一样易读,甚至在某些情况下,甚至说一个示例“完全清楚”。事实并非如此。这个例子很清楚,但并非完全如此,因为它需要更多的思考。map(x).ifPresent(f)根本没有做到那种直观的感觉if(o != null) f(x)。该if版本是完全清楚的。这是人们跳上流行的乐队的另一例,而不是真正的进步。
亚伦

1
请注意,我并不是说使用函数式编程或的优势为零Optional。在此特定情况下,我仅指的是使用optional,而对于可读性则更是如此,因为多个人的行为像这种格式比更具可读性if
亚伦

1
这取决于我们的清晰度概念。尽管您的观点确实很不错,但我想指出的是,对于许多人来说,lambda在某些时候是新奇的。我敢于打赌一个互联网Cookie,很多人说它是isPresent,并得到了令人愉快的安慰:他们现在可以继续更新旧代码,然后再学习lambda语法。另一方面,具有Lisp,Haskell或类似语言经验的人可能很高兴他们现在可以将熟悉的模式应用于Java问题...
HaakonLøtveit18年

@Aaron-清晰度不是Optional类型的重点。我个人发现它们不会降低清晰度,并且在某些情况下(尽管不是这种情况)确实会增加清晰度,但是关键的好处是(只要您避免使用isPresent并且get尽可能多地使用)提高安全性。如果类型Optional<Whatever>不是使用nullable ,则很难编写不检查是否缺少值的错误代码Whatever
朱尔斯

即使您立即取出这些值,只要您不使用get,就必须选择出现缺失值时的处理方式:要么使用orElse()(或orElseGet())提供默认值,要么orElseThrow()抛出选择的值记录问题性质的异常,它比一般的NPE更好,后者在您深入研究堆栈跟踪之前是没有意义的。
朱尔斯

0

我看不到样式2的好处。您仍然需要意识到需要空检查,现在它更大了,因此可读性更差。

更好的样式是,如果employeeServive.getEmployee()返回Optional,那么代码将变为:

Optional<Employee> employeeOptional = employeeServive.getEmployee();
if(employeeOptional.isPresent()){
    Employee employee= employeeOptional.get();
    System.out.println(employee.getId());
}

这样,您将无法调用employloyeeServive.getEmployee()。​​getId(),并且您会注意到应该以某种方式处理可选值。并且,如果在整个代码库中,方法从不返回null(或者几乎从不为您的团队所熟知的此规则例外)返回null,则可以提高针对NPE的安全性。


12
使用时,可选就变得毫无意义isPresent()。我们使用可选的,所以我们不必测试。
candied_orange

2
就像别人所说的,不要使用isPresent()。这是错误的选择方法。使用mapflatMap代替。
Andres F.

1
@CandiedOrange然而,最受好评的答案(声称使用ifPresent)只是另一种测试方法。
immibis

1
@CandiedOrange ifPresent(x)和一样是测试if(present) {x}。返回值无关紧要。
immibis

1
@CandiedOrange尽管如此,您最初说的是“所以我们不必测试”。实际上,当您仍在测试……您不能说“所以我们不必测试”。
亚伦

0

我觉得最大的好处是,如果你的方法签名返回一个Employee检查空。您知道通过该签名可以保证遣返员工。您无需处理失败案例。使用可选件,您知道自己会做。

在代码中,我看到有空检查永远不会失败,因为人们不想通过代码来确定是否可能存在空检查,因此他们会在所有防御性位置抛出空检查。这会使代码变慢一些,但更重要的是,它会使代码更嘈杂。

但是,要使其正常工作,您需要一致地应用此模式。


1
实现此目的的更简单方法是使用@Nullable@ReturnValuesAreNonnullByDefault与静态分析一起使用。
maaartinus

@maaartinus在装饰器注释中以静态分析和文档的形式添加其他工具,是否编译时API支持更简单
雪橇

实际上,添加其他工具确实比较简单,因为它不需要更改代码。而且,无论如何,您都想拥有它,因为它也捕获了其他错误。+++我写了一个简单的脚本文件,添加package-info.java@ReturnValuesAreNonnullByDefault我所有的包,所以我必须做的就是添加@Nullable,你必须切换到Optional。这意味着更少的字符,没有添加的垃圾,也没有运行时开销。将鼠标悬停在该方法上时,不必查看它显示的文档。静态分析工具可以捕获错误以及以后的可空性更改。
maaartinus

我最大的担忧Optional是,它添加了处理空性的另一种方法。如果从一开始就使用Java,就可以了,但是现在只是痛苦了。恕我直言,走科特林的方式要好得多。+++请注意,这List<@Nullable String>仍然是字符串列表,而List<Optional<String>>不是。
maaartinus

0

我的回答是:只是不要。至少要三思而后行,这是否真的是一种进步。

一个表达式(取自另一个答案)像

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

似乎是处理本来可以为空的返回方法的正确功能方法。它看起来不错,它永远不会抛出,也永远不会让您考虑处理“缺席”案件。

看起来比老式的方法更好

Employee employee = employeeService.getEmployee();
if (employee != null) {
    ID id = employee.getId();
    if (id != null) {
        System.out.println(id);
    }
}

很明显,可能有else子句。

功能风格

  • 看起来完全不同(对于不使用它的人来说是不可读的)
  • 调试很麻烦
  • 无法轻松扩展以处理“缺席”情况(orElse并不总是足够的)
  • 误导忽略了“缺席”案件

在任何情况下都不应将其用作灵丹妙药。如果它普遍适用,.则应将操作符的语义替换为?.操作符的语义,将NPE遗忘,并忽略所有问题。


虽然是Java的忠实拥护者,但我必须说,功能风格只是可怜的Java人所替代的声明

employeeService
.getEmployee()
?.getId()
?.apply(id => System.out.println(id));

从其他语言中广为人知。我们是否同意这一说法

  • 是不是(真的)功能?
  • 与老式代码非常相似,是否简单得多?
  • 比功能样式更具可读性?
  • 调试器友好吗?

您似乎在用语言功能描述C#对这一概念的实现,这与Java对核心库类的实现完全不同。他们看起来是一样的我
Caleth

@Caleth没办法。我们可以谈论“简化null安全评估的概念”,但我不会将其称为“概念”。可以说Java使用核心库类实现了相同的事情,但这只是一团糟。刚开始employeeService.getEmployee().getId().apply(id => System.out.println(id));使用安全导航操作符,一次使用使其成为null安全Optional。当然,我们可以将其称为“实现细节”,但是实现很重要。在某些情况下,lamda使代码更简单,更好,但在这里,这只是一个丑陋的滥用。
maaartinus

库类:Optional.of(employeeService).map(EmployeeService::getEmployee).map(Employee::getId).ifPresent(System.out::println);语言功能:employeeService.getEmployee()?.getId()?.apply(id => System.out.println(id));。两种语法,但它们最终看起来非常相似。而“概念”是Optional又名Nullable又名Maybe
Caleth

我不明白为什么您认为其中之一是“丑陋的虐待”,而另一个是好的。我能理解你在想这两个 “丑陋虐待”或两者很好。
凯莱斯(Caleth)'18

@Caleth的不同之处在于,您不必重写两个问号,而必须全部重写。问题不在于语言功能和库类代码之间是否有很大差异。没关系。问题在于原始代码和库类代码相差很多。相同的想法,不同的代码。这太糟糕了。+++被滥用的是处理空性的lamda。Lamdas很好,但Optional不是。可空性仅需要像Kotlin中那样的适当语言支持。另一个问题:List<Optional<String>>与无关List<String>,我们需要将它们关联。
maaartinus

0

可选的是来自功能编程的概念(高阶类型)。使用它可以消除嵌套的null检查,并使您摆脱厄运金字塔。

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.