前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 编程问题:十二、`Optional`

Java 编程问题:十二、`Optional`

作者头像
ApacheCN_飞龙
发布2022-07-11 14:39:39
1.2K0
发布2022-07-11 14:39:39
举报
文章被收录于专栏:信数据得永生信数据得永生

原文:Java Coding Problems 协议:CC BY-NC-SA 4.0 贡献者:飞龙 本文来自【ApacheCN Java 译文集】,自豪地采用谷歌翻译

本章包括 24 个问题,旨在提请您注意使用Optional的几个规则。本节介绍的问题和解决方案基于 Java 语言架构师 Brian Goetz 的定义:

Optional旨在为库方法返回类型提供一种有限的机制,在这种情况下,需要有一种明确的方式来表示无结果,并且使用null表示这种结果极有可能导致错误。”

但有规则的地方也有例外。因此,不要认为应该不惜一切代价遵守(或避免)这里提出的规则(或实践)。一如既往,这取决于问题,你必须评估形势,权衡利弊。

您还可以检查 CDI 插件。这是一个利用Optional模式的 Jakarta EE/JavaEE 容错保护。它的力量在于它的简单。

问题

使用以下问题来测试你的Optional编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:

  1. 初始化Optional:编写一个程序,说明初始化Optional的正确和错误方法。
  2. Optional.get()和缺失值:编写一个程序,举例说明Optional.get()的正确用法和错误用法。
  3. 返回一个已经构造好的默认值:编写一个程序,当没有值时,通过Optional.orElse()方法设置(或返回)一个已经构造好的默认值。
  4. 返回一个不存在的默认值:编写一个程序,当没有值时,通过Optional.orElseGet()方法设置(或返回)一个不存在的默认值。
  5. 抛出NoSuchElementException:编写一个程序,当没有值时抛出NoSuchElementException类型的异常或另一个异常。
  6. Optionalnull引用:编写一个例示Optional.orElse(null)正确用法的程序。
  7. 消耗当前Optional:通过ifPresent()ifPresentElse()编写消耗当前Optional类的程序。
  8. 返回当前Optional类或另一个:假设我们有Optional。编写一个依赖于Optional.or()返回这个Optional(如果它的值存在)或另一个Optional类(如果它的值不存在)的程序。
  9. 通过orElseFoo()链接 Lambda:编写一个程序,举例说明orElse()orElseFoo()的用法,以避免破坏 Lambda 链。
  10. 不要仅仅为了得到一个值而使用Optional:举例说明将Optional方法链接起来的坏做法,目的只是为了得到一些值。
  11. 不要将Optional用于字段:举例说明声明Optional类型字段的不良做法。
  12. 在构造器参数中不要使用Optional:说明在构造器参数中使用Optional的不良做法。
  13. 不要在设置器参数中使用Optional:举例说明在设置器参数中使用Optional的不良做法。
  14. 不要在方法参数中使用Optional:举例说明在方法参数中使用Optional的不良做法。
  15. 不要使用Optional返回空的或null集合或数组:举例说明使用Optional返回空的/null集合或数组的不良做法。
  16. 在集合中避免Optional:在集合中使用Optional可能是一种设计气味。举例说明一个典型的用例和避免集合中的Optional的可能替代方案。
  17. 混淆of()ofNullable():举例说明混淆Optional.of()ofNullable()的潜在后果。
  18. Optional<T>OptionalInt:举例说明非泛型OptionalInt代替Optional<T>的用法。
  19. 断言Optional类的相等:举例说明断言Optional类的相等。
  20. 通过map()flatMap()转换值:写几个代码片段来举例说明Optional.map()flatMap()的用法。
  21. 通过Optional.filter()过滤值:举例说明Optional.filter()基于预定义规则拒绝包装值的用法。
  22. 链接OptionalStreamAPI:举例说明Optional.stream()用于链接OptionalAPI 和StreamAPI。
  23. Optional和身份敏感操作:编写一段代码,支持在Optional的情况下应避免身份敏感的操作。
  24. 返回Optional是否为空的boolean:写两段代码举例说明给定Optional类为空时返回boolean的两种解决方案。

解决方案

以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并在这个页面中试用程序。

226 初始化Optional

初始化Optional应该通过Optional.empty()而不是null进行:

代码语言:javascript
复制
// Avoid
Optional<Book> book = null;

// Prefer
Optional<Book> book = Optional.empty();

因为Optional是一个容器(盒子),所以用null初始化它是没有意义的。

227 Optional.get()和缺失值

因此,如果我们决定调用Optional.get()来获取Optional中包含的值,那么我们不应该按如下方式进行:

代码语言:javascript
复制
Optional<Book> book = ...; // this is prone to be empty

// Avoid
// if "book" is empty then the following code will
// throw a java.util.NoSuchElementException
Book theBook = book.get();

换句话说,在通过Optional.get()获取值之前,我们需要证明值是存在的。解决方案是先调用isPresent(),再调用get()。这样,我们添加了一个检查,允许我们处理缺少值的情况:

代码语言:javascript
复制
Optional<Book> book = ...; // this is prone to be empty

// Prefer
if (book.isPresent()) {
  Book theBook = book.get();
  ... // do something with "theBook"
} else {
  ... // do something that does not call book.get()
}

不过,要记住isPresent()-get()团队信誉不好,所以谨慎使用。考虑检查下一个问题,这些问题为这个团队提供了替代方案。而且,在某个时刻,Optional.get()很可能被否决。

228 返回已构造的默认值

假设我们有一个基于Optional返回结果的方法。如果Optional为空,则该方法返回默认值。如果我们考虑前面的问题,那么一个可能的解决方案可以写成如下:

代码语言:javascript
复制
public static final String BOOK_STATUS = "UNKNOWN";
...
// Avoid
public String findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    return status.get();
  } else {
    return BOOK_STATUS;
  }
}

嗯,这不是一个坏的解决方案,但不是很优雅。一个更简洁和优雅的解决方案将依赖于Optional.orElse()方法。在Optional类为空的情况下,当我们想要设置或返回默认值时,这个方法对于替换isPresent()-get()对非常有用。前面的代码片段可以重写如下:

代码语言:javascript
复制
public static final String BOOK_STATUS = "UNKNOWN";
...
// Prefer
public String findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  return status.orElse(BOOK_STATUS);
}

但要记住,即使涉及的Optional类不是空的,也要对orElse()进行求值。换句话说,orElse()即使不使用它的值也会被求值。既然如此,最好只在其参数是已经构造的值时才依赖orElse()。这样,我们就可以减轻潜在的性能惩罚。下一个问题是orElse()不是正确的选择时解决的。

229 返回不存在的默认值

假设我们有一个方法,它基于Optional类返回结果。如果该Optional类为空,则该方法返回计算值。computeStatus()方法计算此值:

代码语言:javascript
复制
private String computeStatus() {
  // some code used to compute status
}

现在,一个笨拙的解决方案将依赖于isPresent()-get()对,如下所示:

代码语言:javascript
复制
// Avoid
public String findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    return status.get();
  } else {
    return computeStatus();
  }
}

即使这种解决方法很笨拙,也比依赖orElse()方法要好,如下所示:

代码语言:javascript
复制
// Avoid
public String findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  // computeStatus() is called even if "status" is not empty
  return status.orElse(computeStatus());
}

在这种情况下,首选解决方案依赖于Optional.orElseGet()方法。此方法的参数是Supplier;,因此只有在Optional值不存在时才执行。这比orElse()好得多,因为它避免了我们执行当Optional值存在时不应该执行的额外代码。因此,优选方案如下:

代码语言:javascript
复制
// Prefer
public String findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  // computeStatus() is called only if "status" is empty
  return status.orElseGet(this::computeStatus);
}

230 抛出NoSuchElementException

有时,如果Optional为空,我们希望抛出一个异常(例如,NoSuchElementException)。这个问题的笨拙解决方法如下:

代码语言:javascript
复制
// Avoid
public String findStatus() {

  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    return status.get();
  } else {
    throw new NoSuchElementException("Status cannot be found");
  }
}

但一个更优雅的解决方案将依赖于Optional.orElseThrow()方法。此方法的签名orElseThrow(Supplier<? extends X> exceptionSupplier)允许我们给出如下异常(如果存在值,orElseThrow()将返回该值):

代码语言:javascript
复制
// Prefer
public String findStatus() {

  Optional<String> status = ...; // this is prone to be empty

  return status.orElseThrow(
    () -> new NoSuchElementException("Status cannot be found"));
}

或者,另一个异常是,例如,IllegalStateException

代码语言:javascript
复制
// Prefer
public String findStatus() {

  Optional<String> status = ...; // this is prone to be empty

  return status.orElseThrow(
    () -> new IllegalStateException("Status cannot be found"));
}

从 JDK10 开始,Optional富含orElseThrow()风味,没有任何争议。此方法隐式抛出NoSuchElementException

代码语言:javascript
复制
// Prefer (JDK10+)
public String findStatus() {

  Optional<String> status = ...; // this is prone to be empty

  return status.orElseThrow();
}

然而,要注意,在生产中抛出非受检的异常而不带有意义的消息是不好的做法。

231 Optional和空引用

在某些情况下,可以使用接受null引用的方法来利用orElse(null)

本场景的候选对象是 Java 反射 APIMethod.invoke()(见第 7 章、“Java 反射类、接口、构造器、方法、字段”。

Method.invoke()的第一个参数表示要调用此特定方法的对象实例。如果方法是static,那么第一个参数应该是null,因此不需要对象的实例。

假设我们有一个名为Book的类和辅助方法,如下所示。

此方法返回空的Optional类(如果给定方法是static)或包含Book实例的Optional类(如果给定方法是非static):

代码语言:javascript
复制
private static Optional<Book> fetchBookInstance(Method method) {

  if (Modifier.isStatic(method.getModifiers())) {
    return Optional.empty();
  }

  return Optional.of(new Book());
}

调用此方法非常简单:

代码语言:javascript
复制
Method method = Book.class.getDeclaredMethod(...);

Optional<Book> bookInstance = fetchBookInstance(method);

另外,如果Optional为空(即方法为static,则需要将null传递给Method.invoke(),否则传递Book实例。笨拙的解决方案可能依赖于isPresent()-get()对,如下所示:

代码语言:javascript
复制
// Avoid
if (bookInstance.isPresent()) {
  method.invoke(bookInstance.get());
} else {
  method.invoke(null);
}

但这非常适合Optional.orElse(null)。以下代码将解决方案简化为一行代码:

代码语言:javascript
复制
// Prefer
method.invoke(bookInstance.orElse(null));

根据经验,只有当我们有Optional并且需要null引用时,才应该使用orElse(null)。否则,请避开orElse(null)

232 使用当前Optional

有时候,我们想要的只是消费一个类。如果Optional不存在,则无需进行任何操作。不熟练的解决方案将依赖于isPresent()-get()对,如下所示:

代码语言:javascript
复制
// Avoid
public void displayStatus() {
  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    System.out.println(status.get());
  }
}

更好的解决方案依赖于ifPresent(),它以Consumer为参数。当我们只需要消耗现值时,这是一个替代isPresent()-get()对的方法。代码可以重写如下:

代码语言:javascript
复制
// Prefer
public void displayStatus() {
  Optional<String> status = ...; // this is prone to be empty

  status.ifPresent(System.out::println);
}

但在其他情况下,如果Optional不存在,那么我们希望执行一个基于空的操作。基于isPresent()get()对的解决方案如下:

代码语言:javascript
复制
// Avoid
public void displayStatus() {
  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    System.out.println(status.get());
  } else {
    System.out.println("Status not found ...");
  }
}

再说一次,这不是最好的选择。或者,我们可以指望ifPresentOrElse()。这种方法从 JDK9 开始就有了,与ifPresent()方法类似,唯一的区别是它也涵盖了else分支:

代码语言:javascript
复制
// Prefer
public void displayStatus() {
  Optional<String> status = ...; // this is prone to be empty

  status.ifPresentOrElse(System.out::println,
    () -> System.out.println("Status not found ..."));
}

233 返回当前Optional类或其他类

让我们考虑一个返回Optional类的方法。主要地,这个方法计算一个Optional类,如果它不是空的,那么它只返回这个Optional类。否则,如果计算出的Optional类为空,那么我们执行一些其他操作,该操作也返回Optional类。

isPresent()-get()对可以按如下方式进行(应避免这样做):

代码语言:javascript
复制
private final static String BOOK_STATUS = "UNKNOWN";
...
// Avoid
public Optional<String> findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  if (status.isPresent()) {
    return status;
  } else {
    return Optional.of(BOOK_STATUS);
  }
}

或者,我们应该避免以下构造:

代码语言:javascript
复制
return Optional.of(status.orElse(BOOK_STATUS));
return Optional.of(status.orElseGet(() -> (BOOK_STATUS)));

从 JDK9 开始就有了最佳的解决方案,它由Optional.or()方法组成。此方法能够返回描述值的Optional。否则,返回给定的Supplier函数产生的Optional(产生要返回的Optional的供给函数):

代码语言:javascript
复制
private final static String BOOK_STATUS = "UNKNOWN";
...
// Prefer
public Optional<String> findStatus() {
  Optional<String> status = ...; // this is prone to be empty

  return status.or(() -> Optional.of(BOOK_STATUS));
}

234 通过orElseFoo()链接 Lambda

一些特定于 Lambda 表达式的操作返回Optional(例如,findFirst()findAny()reduce()等)。试图通过isPresent()-get()对来处理这些Optional类是一个麻烦的解决方案,因为我们必须打破 Lambda 链,通过if-else块添加一些条件代码,并考虑恢复该链。

以下代码片段显示了这种做法:

代码语言:javascript
复制
private static final String NOT_FOUND = "NOT FOUND";

List<Book> books...;
...
// Avoid
public String findFirstCheaperBook(int price) {

  Optional<Book> book = books.stream()
    .filter(b -> b.getPrice()<price)
    .findFirst();

  if (book.isPresent()) {
    return book.get().getName();
  } else {
    return NOT_FOUND;
  }
}

再往前一步,我们可能会得到如下结果:

代码语言:javascript
复制
// Avoid
public String findFirstCheaperBook(int price) {

  Optional<Book> book = books.stream()
    .filter(b -> b.getPrice()<price)
    .findFirst();

  return book.map(Book::getName)
    .orElse(NOT_FOUND);
}

orElse()代替isPresent()-get()对更为理想。但如果我们直接在 Lambda 链中使用orElse()(和orElseFoo(),避免代码中断,则效果会更好:

代码语言:javascript
复制
private static final String NOT_FOUND = "NOT FOUND";
...
// Prefer
public String findFirstCheaperBook(int price) {

  return books.stream()
    .filter(b -> b.getPrice()<price)
    .findFirst()
    .map(Book::getName)
    .orElse(NOT_FOUND);
}

我们再来一个问题。

这一次,我们有几本书的作者,我们要检查某一本书是否是作者写的。如果我们的作者没有写给定的书,那么我们想抛出NoSuchElementException

一个非常糟糕的解决方案如下:

代码语言:javascript
复制
// Avoid
public void validateAuthorOfBook(Book book) {
  if (!author.isPresent() ||
    !author.get().getBooks().contains(book)) {
    throw new NoSuchElementException();
  }
}

另一方面,使用orElseThrow()可以非常优雅地解决问题:

代码语言:javascript
复制
// Prefer
public void validateAuthorOfBook(Book book) {
  author.filter(a -> a.getBooks().contains(book))
    .orElseThrow();
}

235 不要仅为获取值而使用Optional

这个问题从不要使用类别的一系列问题开始。不要使用类别试图防止过度使用,并给出了一些可以避免我们很多麻烦的规则。然而,规则也有例外。因此,不要认为应不惜一切代价避免所提出的规则。一如既往,这取决于问题。

Optional的情况下,一个常见的场景是为了获得一些值而链接其方法。

避免这种做法,并依赖简单和简单的代码。换句话说,避免做类似以下代码片段的操作:

代码语言:javascript
复制
public static final String BOOK_STATUS = "UNKNOWN";
...
// Avoid
public String findStatus() {
  // fetch a status prone to be null
  String status = ...;

  return Optional.ofNullable(status).orElse(BOOK_STATUS);
}

并使用简单的if-else块或三元运算符(对于简单情况):

代码语言:javascript
复制
// Prefer
public String findStatus() {
  // fetch a status prone to be null
  String status = null;

  return status == null ? BOOK_STATUS : status;
}

236 不要为字段使用Optional

不要使用类别继续下面的语句-Optional不打算用于字段,它不实现Serializable

Optional类肯定不打算用作 JavaBean 的字段。所以,不要这样做:

代码语言:javascript
复制
// Avoid
public class Book {

  [access_modifier][static][final]
    Optional<String> title;
  [access_modifier][static][final]
    Optional<String> subtitle = Optional.empty();
  ...
}

但要做到:

代码语言:javascript
复制
// Prefer
public class Book {

  [access_modifier][static][final] String title;
  [access_modifier][static][final] String subtitle = "";
  ...
}

237 不要在构造器参数中使用Optional

不要使用类别继续使用另一种与使用Optional的意图相反的场景。请记住,Optional表示对象的容器;因此,Optional添加了另一个抽象级别。换句话说,Optional的不当使用只是增加了额外的样板代码。

检查Optional的以下用例,可以看出这一点(此代码违反了前面的“不要对字段使用Optional”一节):

代码语言:javascript
复制
// Avoid
public class Book {

  // cannot be null
  private final String title; 

  // optional field, cannot be null
  private final Optional<String> isbn;

  public Book(String title, Optional<String> isbn) {
    this.title = Objects.requireNonNull(title,
      () -> "Title cannot be null");

    if (isbn == null) {
      this.isbn = Optional.empty();
    } else {
      this.isbn = isbn;
    }

    // or
    this.isbn = Objects.requireNonNullElse(isbn, Optional.empty());
  }

  public String getTitle() {
    return title;
  }

  public Optional<String> getIsbn() {
    return isbn;
  }
}

我们可以通过从字段和构造器参数中删除Optional来修复此代码,如下所示:

代码语言:javascript
复制
// Prefer
public class Book {

  private final String title; // cannot be null
  private final String isbn; // can be null

  public Book(String title, String isbn) {
    this.title = Objects.requireNonNull(title,
      () -> "Title cannot be null");
    this.isbn = isbn;
  }

  public String getTitle() {
    return title;
  }

  public Optional<String> getIsbn() {
    return Optional.ofNullable(isbn);
  }
}

isbn的获取器返回Optional。但是不要将此示例视为以这种方式转换所有获取器的规则。有些获取器返回集合或数组,在这种情况下,他们更喜欢返回空的集合/数组,而不是返回Optional。使用此技术并记住 BrianGoetz(Java 语言架构师)的声明:

“我认为它肯定会被常规地过度用作获取器的返回值。” ——布赖恩·格茨(Brian Goetz)

238 不要在设置器参数中使用Optional

不要使用类别继续使用一个非常诱人的场景,包括在设置器参数中使用Optional。应避免使用以下代码,因为它添加了额外的样板代码,并且违反了“请勿将Optional用于字段”部分(请检查setIsbn()方法):

代码语言:javascript
复制
// Avoid
public class Book {

  private Optional<String> isbn;

  public Optional<String> getIsbn() {
    return isbn;
  }

  public void setIsbn(Optional<String> isbn) {
    if (isbn == null) {
      this.isbn = Optional.empty();
    } else {
      this.isbn = isbn;
    }

    // or
    this.isbn = Objects.requireNonNullElse(isbn, Optional.empty());
  }
}

我们可以通过从字段和设置器的参数中删除Optional来修复此代码,如下所示:

代码语言:javascript
复制
// Prefer
public class Book {

  private String isbn;

  public Optional<String> getIsbn() {
    return Optional.ofNullable(isbn);
  }

  public void setIsbn(String isbn) {
    this.isbn = isbn;
  }
}

通常,这种糟糕的做法在 JPA 实体中用于持久属性(将实体属性映射为Optional)。然而,在域模型实体中使用Optional是可能的。

239 不要在方法参数中使用Optional

不要使用类别继续使用Optional的另一个常见错误。这次让我们讨论一下方法参数中Optional的用法。

在方法参数中使用Optional只是另一个用例,可能会导致代码变得不必要的复杂。主要是建议承担null检查参数的责任,而不是相信调用方会创建Optional类,尤其是空Optional类。这种糟糕的做法会使代码变得混乱,而且仍然容易出现NullPointerException。调用者仍可通过null。所以你刚才又开始检查null参数了。

请记住,Optional只是另一个物体(容器),并不便宜。Optional消耗裸引用内存的四倍!

作为结论,在执行以下操作之前,请三思而后行:

代码语言:javascript
复制
// Avoid
public void renderBook(Format format,
  Optional<Renderer> renderer, Optional<String> size) {

  Objects.requireNonNull(format, "Format cannot be null");

  Renderer bookRenderer = renderer.orElseThrow(
    () -> new IllegalArgumentException("Renderer cannot be empty")
  );

  String bookSize = size.orElseGet(() -> "125 x 200");
  ...
}

检查创建所需Optional类的此方法的以下调用。但是,很明显,通过null也是可能的,会导致NullPointerException,但这意味着你故意挫败了Optional——不要想通过null参数的检查来污染前面的代码,这真是个坏主意:

代码语言:javascript
复制
Book book = new Book();

// Avoid
book.renderBook(new Format(),
  Optional.of(new CoolRenderer()), Optional.empty());

// Avoid
// lead to NPE
book.renderBook(new Format(),
  Optional.of(new CoolRenderer()), null);

我们可以通过删除Optional类来修复此代码,如下所示:

代码语言:javascript
复制
// Prefer
public void renderBook(Format format, 
    Renderer renderer, String size) {

  Objects.requireNonNull(format, "Format cannot be null");
  Objects.requireNonNull(renderer, "Renderer cannot be null");

  String bookSize = Objects.requireNonNullElseGet(
    size, () -> "125 x 200");
  ...
}

这次,这个方法的调用不强制创建Optional

代码语言:javascript
复制
Book book = new Book();

// Prefer
book.renderBook(new Format(), new CoolRenderer(), null);

当一个方法可以接受可选参数时,依赖于旧式方法重载,而不是依赖于Optional

240 不要使用Optional返回空的或null集合或数组

此外,在不要使用类别中,让我们来讨论如何使用Optional作为包装空集合或null集合或数组的返回类型。

返回包装空集合/数组的Optionalnull集合/数组可能由干净的轻量级代码组成。查看以下代码:

代码语言:javascript
复制
// Avoid
public Optional<List<Book>> fetchBooksByYear(int year) {
  // fetching the books may return null
  List<Book> books = ...;

  return Optional.ofNullable(books);
}

Optional<List<Book>> books = author.fetchBooksByYear(2021);

// Avoid
public Optional<Book[]> fetchBooksByYear(int year) {
  // fetching the books may return null
  Book[] books = ...;

  return Optional.ofNullable(books);
}

Optional<Book[]> books = author.fetchBooksByYear(2021);

我们可以通过删除不必要的Optional来清除此代码,然后依赖空集合(例如,Collections.emptyList()emptyMap()emptySet())和数组(例如,new String[0])。这是更好的解决方案:

代码语言:javascript
复制
// Prefer
public List<Book> fetchBooksByYear(int year) {
  // fetching the books may return null
  List<Book> books = ...;

  return books == null ? Collections.emptyList() : books;
}

List<Book> books = author.fetchBooksByYear(2021);

// Prefer
public Book[] fetchBooksByYear(int year) {
  // fetching the books may return null
  Book[] books = ...;

  return books == null ? new Book[0] : books;
}

Book[] books = author.fetchBooksByYear(2021);

如果需要区分缺少的集合/数组和空集合/数组,则为缺少的集合/数组抛出异常。

241 避免集合中的Optional

依靠集合中的Optional可能是一种设计的味道。再花 30 分钟重新评估问题并找到更好的解决方案。

前面的语句尤其在Map的情况下是有效的,当这个决定背后的原因听起来像这样时,Map返回null如果一个键没有映射或者null映射到了这个键,那么我无法判断这个键是不存在还是缺少值。我将通过Optional.ofNullable()包装数值并完成!

但是,如果Optional<Foo>Map填充了null值,缺少Optional值,甚至Optional对象包含了其他内容,而不是Foo,我们将进一步决定什么呢?我们不是把最初的问题嵌套到另一层吗?表演罚怎么样?Optional不是免费的;它只是另一个消耗内存的对象,需要收集。

所以,让我们考虑一个应该避免的解决方案:

代码语言:javascript
复制
private static final String NOT_FOUND = "NOT FOUND";
...
// Avoid
Map<String, Optional<String>> isbns = new HashMap<>();
isbns.put("Book1", Optional.ofNullable(null));
isbns.put("Book2", Optional.ofNullable("123-456-789"));
...
Optional<String> isbn = isbns.get("Book1");

if (isbn == null) {
  System.out.println("This key cannot be found");
} else {
  String unwrappedIsbn = isbn.orElse(NOT_FOUND);
  System.out.println("Key found, Value: " + unwrappedIsbn);
}

更好更优雅的解决方案可以依赖于 JDK8,getOrDefault()如下:

代码语言:javascript
复制
private static String get(Map<String, String> map, String key) {
  return map.getOrDefault(key, NOT_FOUND);
}

Map<String, String> isbns = new HashMap<>();
isbns.put("Book1", null);
isbns.put("Book2", "123-456-789");
...
String isbn1 = get(isbns, "Book1"); // null
String isbn2 = get(isbns, "Book2"); // 123-456-789
String isbn3 = get(isbns, "Book3"); // NOT FOUND

其他解决方案可依赖于以下内容:

  • containsKey()方法
  • 通过扩展HashMap来实现琐碎的实现
  • JDK8computeIfAbsent()方法
  • ApacheCommon DefaultedMap

我们可以得出结论,总有比在集合中使用Optional更好的解决方案。

但是前面讨论的用例并不是最坏的场景。这里还有两个必须避免的问题:

代码语言:javascript
复制
Map<Optional<String>, String> items = new HashMap<>();
Map<Optional<String>, Optional<String>> items = new HashMap<>();

242 将of()ofNullable()混淆

混淆或误用Optional.of()代替Optional.ofNullable(),反之亦然,会导致怪异行为,甚至NullPointerException

Optional.of(null)会抛出NullPointerException,但是Optional.ofNullable(null)会产生Optional.empty

请检查以下失败的尝试,以编写一段代码来避免NullPointerException

代码语言:javascript
复制
// Avoid
public Optional<String> isbn(String bookId) {
  // the fetched "isbn" can be null for the given "bookId"
  String isbn = ...;

  return Optional.of(isbn); // this throws NPE if "isbn" is null :(
}

但是,最有可能的是,我们实际上想要使用ofNullable(),如下所示:

代码语言:javascript
复制
// Prefer
public Optional<String> isbn(String bookId) {
  // the fetched "isbn" can be null for the given "bookId"
  String isbn = ...;

  return Optional.ofNullable(isbn);
}

ofNullable()代替of()不是灾难,但可能会造成一些混乱,没有任何价值。检查以下代码:

代码语言:javascript
复制
// Avoid
// ofNullable() doesn't add any value
return Optional.ofNullable("123-456-789");

// Prefer
return Optional.of("123-456-789"); // no risk to NPE

这是另一个问题。假设我们想把一个空的String对象转换成一个空的Optional。我们可以认为适当的解决方案将依赖于of(),如下所示:

代码语言:javascript
复制
// Avoid
Optional<String> result = Optional.of(str)
 .filter(not(String::isEmpty));

但是请记住,String可以是null。此解决方案适用于空字符串或非空字符串,但不适用于null字符串。因此,ofNullable()给出了合适的解决方案,如下:

代码语言:javascript
复制
// Prefer
Optional<String> result = Optional.ofNullable(str)
  .filter(not(String::isEmpty));

243 Optional<T>OptionalInt

如果没有使用装箱基本类型的具体原因,则宜避免Optional<T>并依赖非通用OptionalIntOptionalLongOptionalDouble型。

装箱和拆箱是昂贵的操作,容易导致性能损失。为了消除这种风险,我们可以依赖于OptionalIntOptionalLongOptionalDouble。这些是intlongdouble原始类型的包装器。

因此,请避免使用以下(及类似)解决方案:

代码语言:javascript
复制
// Avoid
Optional<Integer> priceInt = Optional.of(50);
Optional<Long> priceLong = Optional.of(50L);
Optional<Double> priceDouble = Optional.of(49.99d);

更喜欢以下解决方案:

代码语言:javascript
复制
// Prefer
// unwrap via getAsInt()
OptionalInt priceInt = OptionalInt.of(50);

// unwrap via getAsLong()
OptionalLong priceLong = OptionalLong.of(50L);

// unwrap via getAsDouble()
OptionalDouble priceDouble = OptionalDouble.of(49.99d);

244 Optional.assertEquals()

assertEquals()中有两个Optional对象不需要展开值。这是适用的,因为Optional.equals()比较包裹值,而不是Optional对象。这是Optional.equals()的源代码:

代码语言:javascript
复制
@Override
public boolean equals(Object obj) {

  if (this == obj) {
    return true;
  }

  if (!(obj instanceof Optional)) {
    return false;
  }

  Optional<?> other = (Optional<?>) obj;

  return Objects.equals(value, other.value);
}

假设我们有两个Optional对象:

代码语言:javascript
复制
Optional<String> actual = ...;
Optional<String> expected = ...;

// or
Optional actual = ...;
Optional expected = ...; 

建议避免进行以下测试:

代码语言:javascript
复制
// Avoid
@Test
public void givenOptionalsWhenTestEqualityThenTrue() 
    throws Exception {

  assertEquals(expected.get(), actual.get());
}

如果预期和/或实际为空,get()方法将导致NoSuchElementException类型的异常。

最好使用以下测试:

代码语言:javascript
复制
// Prefer
@Test
public void givenOptionalsWhenTestEqualityThenTrue() 
    throws Exception {

  assertEquals(expected, actual);
}

245 通过map()flatMap()转换值

Optional.map()flatMap()方法便于转换Optional值。

map()方法将函数参数应用于值,然后返回包装在Optional对象中的结果。flatMap()方法将函数参数应用于值,然后直接返回结果。

假设我们有Optional<String>,我们想把这个String从小写转换成大写。一个没有灵感的解决方案可以写如下:

代码语言:javascript
复制
Optional<String> lowername = ...; // may be empty as well

// Avoid
Optional<String> uppername;

if (lowername.isPresent()) {
  uppername = Optional.of(lowername.get().toUpperCase());
} else {
  uppername = Optional.empty();
}

更具启发性的解决方案(在一行代码中)将依赖于Optional.map(),如下所示:

代码语言:javascript
复制
// Prefer
Optional<String> uppername = lowername.map(String::toUpperCase);

map()方法也可以用来避免破坏 Lambda 链。让我们考虑一下List<Book>,我们想找到第一本便宜 50 美元的书,如果有这样一本书,就把它的书名改成大写。同样,没有灵感的解决方案如下:

代码语言:javascript
复制
private static final String NOT_FOUND = "NOT FOUND";
List<Book> books = Arrays.asList();
...
// Avoid
Optional<Book> book = books.stream()
  .filter(b -> b.getPrice()<50)
  .findFirst();

String title;
if (book.isPresent()) {
  title = book.get().getTitle().toUpperCase();
} else {
  title = NOT_FOUND;
}

依靠map(),我们可以通过以下 Lambda 链来实现:

代码语言:javascript
复制
// Prefer
String title = books.stream()
  .filter(b -> b.getPrice()<50)
  .findFirst()
  .map(Book::getTitle)
  .map(String::toUpperCase)
  .orElse(NOT_FOUND);

在前面的示例中,getTitle()方法是一个经典的获取器,它将书名返回为String。但是让我们修改这个获取器以返回Optional

代码语言:javascript
复制
public Optional<String> getTitle() {
  return ...;
}

这次我们不能使用map(),因为map(Book::getTitle)会返回Optional<Optional<String>>而不是Optional<String>。但是如果我们依赖于flatMap(),那么它的返回将不会被包装在一个额外的Optional对象中:

代码语言:javascript
复制
// Prefer
String title = books.stream()
  .filter(b -> b.getPrice()<50)
  .findFirst()
  .flatMap(Book::getTitle)
  .map(String::toUpperCase)
  .orElse(NOT_FOUND);

所以,Optional.map()将变换的结果包装在Optional对象中。如果这个结果是Optional本身,那么我们就得到Optional<Optional<...>>。另一方面,flatMap()不会将结果包装在另一个Optional对象中。

246 通过Optional.filter()过滤值

使用Optional.filter()接受或拒绝包装值是一种非常方便的方法,因为它可以在不显式展开值的情况下完成。我们只需传递谓词(条件)作为参数,如果满足条件,则得到一个Optional对象(初始Optional对象,如果条件不满足则得到空的Optional对象)。

让我们考虑以下未经启发的方法来验证书的长度 ISBN:

代码语言:javascript
复制
// Avoid
public boolean validateIsbnLength(Book book) {

  Optional<String> isbn = book.getIsbn();

  if (isbn.isPresent()) {
    return isbn.get().length() > 10;
  }

  return false;
}

前面的解决方案依赖于显式地展开Optional值。但是如果我们依赖于Optional.filter(),我们可以不使用这种显式展开,如下所示:

代码语言:javascript
复制
// Prefer
public boolean validateIsbnLength(Book book) {

  Optional<String> isbn = book.getIsbn();

  return isbn.filter((i) -> i.length() > 10)
    .isPresent();
}

Optional.filter() is also useful for avoiding breaking lambda chains.

247 链接Optional和流 API

从 JDK9 开始,我们可以通过应用Optional.stream()方法将Optional实例引用为Stream

当我们必须链接OptionalStreamAPI 时,这非常有用。Optional.stream()方法返回一个元素的StreamOptional的值)或空的Stream(如果Optional没有值)。此外,我们可以使用StreamAPI 中提供的所有方法。

假设我们有一个按 ISBN 取书的方法(如果没有书与给定的 ISBN 匹配,那么这个方法返回一个空的Optional对象):

代码语言:javascript
复制
public Optional<Book> fetchBookByIsbn(String isbn) {
  // fetching book by the given "isbn" can return null
  Book book = ...;

  return Optional.ofNullable(book);
}

除此之外,我们循环一个 ISBN 的List,返回BookList如下(每个 ISBN 通过fetchBookByIsbn()方法传递):

代码语言:javascript
复制
// Avoid
public List<Book> fetchBooks(List<String> isbns) {

  return isbns.stream()
    .map(this::fetchBookByIsbn)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(toList());
}

这里的重点是以下两行代码:

代码语言:javascript
复制
.filter(Optional::isPresent)
.map(Optional::get)

因为fetchBookByIsbn()方法可以返回空的Optional类,所以我们必须确保从最终结果中消除它们。为此,我们调用Stream.filter()并将Optional.isPresent()函数应用于fetchBookByIsbn()返回的每个Optional对象。所以,在过滤之后,我们只有Optional类具有当前值。此外,我们应用Stream.map()方法将这些Optional类解包到Book。最后,我们收集List中的Book对象。

但我们可以更优雅地使用Optional.stream()来完成同样的事情,具体如下:

代码语言:javascript
复制
// Prefer
public List<Book> fetchBooksPrefer(List<String> isbns) {

  return isbns.stream()
    .map(this::fetchBookByIsbn)
    .flatMap(Optional::stream)
    .collect(toList());
}

实际上,在这样的情况下,我们可以用Optional.stream()来替换filter()map()flatMap()

fetchBookByIsbn()返回的Optional<Book>每回Optional.stream()将导致Stream<Book>中包含单个Book对象或无物(空流)。如果Optional<Book>不包含值(为空),则Stream<Book>也为空。依靠flatMap()代替map()将避免Stream<Stream<Book>>型的结果。

作为奖励,我们可以将Optional转换为List,如下所示:

代码语言:javascript
复制
public static<T> List<T> optionalToList(Optional<T> optional) {
  return optional.stream().collect(toList());
}

248 Optional和身份敏感操作

身份敏感操作包括引用相等(==)、基于身份哈希或同步。

Optional类是基于值的类,如LocalDateTime,因此应该避免身份敏感操作。

例如,让我们通过==测试两个Optional类的相等性:

代码语言:javascript
复制
Book book = new Book();
Optional<Book> op1 = Optional.of(book);
Optional<Book> op2 = Optional.of(book);

// Avoid
// op1 == op2 => false, expected true
if (op1 == op2) {
  System.out.println("op1 is equal with op2, (via ==)");
} else {
  System.out.println("op1 is not equal with op2, (via ==)");
}

这将产生以下输出:

代码语言:javascript
复制
op1 is not equal with op2, (via ==)

因为op1op2不是对同一个对象的引用,所以它们不相等,所以不符合==的实现。

为了比较这些值,我们需要依赖于equals(),如下所示:

代码语言:javascript
复制
// Prefer
if (op1.equals(op2)) {
  System.out.println("op1 is equal with op2, (via equals())");
} else {
  System.out.println("op1 is not equal with op2, (via equals())");
}

这将产生以下输出:

代码语言:javascript
复制
op1 is equal with op2, (via equals())

identity-sensitive操作的上下文中,千万不要这样做(认为Optional是一个基于值的类,这样的类不应该用于锁定更多细节,请参见这个页面

代码语言:javascript
复制
Optional<Book> book = Optional.of(new Book());
synchronized(book) {
  ...
}

249 如果Optional类为空,则返回布尔值

假设我们有以下简单的方法:

代码语言:javascript
复制
public static Optional<Cart> fetchCart(long userId) {
  // the shopping cart of the given "userId" can be null
  Cart cart = ...;

  return Optional.ofNullable(cart);
}

现在,我们要编写一个名为cartIsEmpty()的方法来调用fetchCart()方法,如果获取的购物车为空,则返回一个标志,即true。在 JDK11 之前,我们可以基于Optional.isPresent()实现这个方法,如下所示:

代码语言:javascript
复制
// Avoid (after JDK11)
public static boolean cartIsEmpty(long id) {
  Optional<Cart> cart = fetchCart(id);

  return !cart.isPresent();
}

这个解决方案可以很好地工作,但不是很有表现力。我们通过存在来检查空虚,我们必须否定isPresent()的结果。

自 JDK11 以来,Optional类被一个名为isEmpty()的新方法所丰富。顾名思义,这是一个标志方法,如果测试的Optional类为空,它将返回true。因此,我们可以通过以下方式提高解决方案的表达能力:

代码语言:javascript
复制
// Prefer (after JDK11)
public static boolean cartIsEmpty(long id) {
  Optional<Cart> cart = fetchCart(id);

  return cart.isEmpty();
}

总结

完成!这是本章的最后一个问题。此时,您应该拥有正确使用Optional所需的所有参数

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题
  • 解决方案
  • 226 初始化Optional
  • 227 Optional.get()和缺失值
  • 228 返回已构造的默认值
  • 229 返回不存在的默认值
  • 230 抛出NoSuchElementException
  • 231 Optional和空引用
  • 232 使用当前Optional类
  • 233 返回当前Optional类或其他类
  • 234 通过orElseFoo()链接 Lambda
  • 235 不要仅为获取值而使用Optional
  • 236 不要为字段使用Optional
  • 237 不要在构造器参数中使用Optional
  • 238 不要在设置器参数中使用Optional
  • 239 不要在方法参数中使用Optional
  • 240 不要使用Optional返回空的或null集合或数组
  • 241 避免集合中的Optional
  • 242 将of()与ofNullable()混淆
  • 243 Optional<T>与OptionalInt
  • 244 Optional.assertEquals()
  • 245 通过map()和flatMap()转换值
  • 246 通过Optional.filter()过滤值
  • 247 链接Optional和流 API
  • 248 Optional和身份敏感操作
  • 249 如果Optional类为空,则返回布尔值
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档