本章包括 24 个问题,旨在提请您注意使用Optional
的几个规则。本节介绍的问题和解决方案基于 Java 语言架构师 Brian Goetz 的定义:
“Optional
旨在为库方法返回类型提供一种有限的机制,在这种情况下,需要有一种明确的方式来表示无结果,并且使用null
表示这种结果极有可能导致错误。”
但有规则的地方也有例外。因此,不要认为应该不惜一切代价遵守(或避免)这里提出的规则(或实践)。一如既往,这取决于问题,你必须评估形势,权衡利弊。
您还可以检查 CDI 插件。这是一个利用Optional
模式的 Jakarta EE/JavaEE 容错保护。它的力量在于它的简单。
问题
使用以下问题来测试你的Optional
编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
- 初始化
Optional
:编写一个程序,说明初始化Optional
的正确和错误方法。 Optional.get()
和缺失值:编写一个程序,举例说明Optional.get()
的正确用法和错误用法。- 返回一个已经构造好的默认值:编写一个程序,当没有值时,通过
Optional.orElse()
方法设置(或返回)一个已经构造好的默认值。 - 返回一个不存在的默认值:编写一个程序,当没有值时,通过
Optional.orElseGet()
方法设置(或返回)一个不存在的默认值。 - 抛出
NoSuchElementException
:编写一个程序,当没有值时抛出NoSuchElementException
类型的异常或另一个异常。 Optional
和null
引用:编写一个例示Optional.orElse(null)
正确用法的程序。- 消耗当前
Optional
类:通过ifPresent()
和ifPresentElse()
编写消耗当前Optional
类的程序。 - 返回当前
Optional
类或另一个:假设我们有Optional
。编写一个依赖于Optional.or()
返回这个Optional
(如果它的值存在)或另一个Optional
类(如果它的值不存在)的程序。 - 通过
orElseFoo()
链接 Lambda:编写一个程序,举例说明orElse()
和orElseFoo()
的用法,以避免破坏 Lambda 链。 - 不要仅仅为了得到一个值而使用
Optional
:举例说明将Optional
方法链接起来的坏做法,目的只是为了得到一些值。 - 不要将
Optional
用于字段:举例说明声明Optional
类型字段的不良做法。 - 在构造器参数中不要使用
Optional
:说明在构造器参数中使用Optional
的不良做法。 - 不要在设置器参数中使用
Optional
:举例说明在设置器参数中使用Optional
的不良做法。 - 不要在方法参数中使用
Optional
:举例说明在方法参数中使用Optional
的不良做法。 - 不要使用
Optional
返回空的或null
集合或数组:举例说明使用Optional
返回空的/null
集合或数组的不良做法。 - 在集合中避免
Optional
:在集合中使用Optional
可能是一种设计气味。举例说明一个典型的用例和避免集合中的Optional
的可能替代方案。 - 混淆
of()
与ofNullable()
:举例说明混淆Optional.of()
与ofNullable()
的潜在后果。 Optional<T>
与OptionalInt
:举例说明非泛型OptionalInt
代替Optional<T>
的用法。- 断言
Optional
类的相等:举例说明断言Optional
类的相等。 - 通过
map()
和flatMap()
转换值:写几个代码片段来举例说明Optional.map()
和flatMap()
的用法。 - 通过
Optional.filter()
过滤值:举例说明Optional.filter()
基于预定义规则拒绝包装值的用法。 - 链接
Optional
和Stream
API:举例说明Optional.stream()
用于链接Optional
API 和Stream
API。 Optional
和身份敏感操作:编写一段代码,支持在Optional
的情况下应避免身份敏感的操作。- 返回
Optional
是否为空的boolean
:写两段代码举例说明给定Optional
类为空时返回boolean
的两种解决方案。
解决方案
以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并在这个页面中试用程序。
226 初始化Optional
初始化Optional
应该通过Optional.empty()
而不是null
进行:
// Avoid Optional<Book> book = null; // Prefer Optional<Book> book = Optional.empty();
因为Optional
是一个容器(盒子),所以用null
初始化它是没有意义的。
227 Optional.get()
和缺失值
因此,如果我们决定调用Optional.get()
来获取Optional
中包含的值,那么我们不应该按如下方式进行:
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()
。这样,我们添加了一个检查,允许我们处理缺少值的情况:
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
为空,则该方法返回默认值。如果我们考虑前面的问题,那么一个可能的解决方案可以写成如下:
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()
对非常有用。前面的代码片段可以重写如下:
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()
方法计算此值:
private String computeStatus() { // some code used to compute status }
现在,一个笨拙的解决方案将依赖于isPresent()
-get()
对,如下所示:
// Avoid public String findStatus() { Optional<String> status = ...; // this is prone to be empty if (status.isPresent()) { return status.get(); } else { return computeStatus(); } }
即使这种解决方法很笨拙,也比依赖orElse()
方法要好,如下所示:
// 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
值存在时不应该执行的额外代码。因此,优选方案如下:
// 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
)。这个问题的笨拙解决方法如下:
// 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()
将返回该值):
// Prefer public String findStatus() { Optional<String> status = ...; // this is prone to be empty return status.orElseThrow( () -> new NoSuchElementException("Status cannot be found")); }
或者,另一个异常是,例如,IllegalStateException
:
// 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
:
// 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
):
private static Optional<Book> fetchBookInstance(Method method) { if (Modifier.isStatic(method.getModifiers())) { return Optional.empty(); } return Optional.of(new Book()); }
调用此方法非常简单:
Method method = Book.class.getDeclaredMethod(...); Optional<Book> bookInstance = fetchBookInstance(method);
另外,如果Optional
为空(即方法为static
,则需要将null
传递给Method.invoke()
,否则传递Book
实例。笨拙的解决方案可能依赖于isPresent()
-get()
对,如下所示:
// Avoid if (bookInstance.isPresent()) { method.invoke(bookInstance.get()); } else { method.invoke(null); }
但这非常适合Optional.orElse(null)
。以下代码将解决方案简化为一行代码:
// Prefer method.invoke(bookInstance.orElse(null));
根据经验,只有当我们有Optional
并且需要null
引用时,才应该使用orElse(null)
。否则,请避开orElse(null)
。
232 使用当前Optional
类
有时候,我们想要的只是消费一个类。如果Optional
不存在,则无需进行任何操作。不熟练的解决方案将依赖于isPresent()
-get()
对,如下所示:
// 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()
对的方法。代码可以重写如下:
// Prefer public void displayStatus() { Optional<String> status = ...; // this is prone to be empty status.ifPresent(System.out::println); }
但在其他情况下,如果Optional
不存在,那么我们希望执行一个基于空的操作。基于isPresent()
—get()
对的解决方案如下:
// 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
分支:
// 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()
对可以按如下方式进行(应避免这样做):
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); } }
或者,我们应该避免以下构造:
return Optional.of(status.orElse(BOOK_STATUS)); return Optional.of(status.orElseGet(() -> (BOOK_STATUS)));
从 JDK9 开始就有了最佳的解决方案,它由Optional.or()
方法组成。此方法能够返回描述值的Optional
。否则,返回给定的Supplier
函数产生的Optional
(产生要返回的Optional
的供给函数):
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
块添加一些条件代码,并考虑恢复该链。
以下代码片段显示了这种做法:
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; } }
再往前一步,我们可能会得到如下结果:
// 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()
,避免代码中断,则效果会更好:
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
。
一个非常糟糕的解决方案如下:
// Avoid public void validateAuthorOfBook(Book book) { if (!author.isPresent() || !author.get().getBooks().contains(book)) { throw new NoSuchElementException(); } }
另一方面,使用orElseThrow()
可以非常优雅地解决问题:
// Prefer public void validateAuthorOfBook(Book book) { author.filter(a -> a.getBooks().contains(book)) .orElseThrow(); }
235 不要仅为获取值而使用Optional
这个问题从不要使用类别的一系列问题开始。不要使用类别试图防止过度使用,并给出了一些可以避免我们很多麻烦的规则。然而,规则也有例外。因此,不要认为应不惜一切代价避免所提出的规则。一如既往,这取决于问题。
在Optional
的情况下,一个常见的场景是为了获得一些值而链接其方法。
避免这种做法,并依赖简单和简单的代码。换句话说,避免做类似以下代码片段的操作:
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
块或三元运算符(对于简单情况):
// 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 的字段。所以,不要这样做:
// Avoid public class Book { [access_modifier][static][final] Optional<String> title; [access_modifier][static][final] Optional<String> subtitle = Optional.empty(); ... }
但要做到:
// Prefer public class Book { [access_modifier][static][final] String title; [access_modifier][static][final] String subtitle = ""; ... }
237 不要在构造器参数中使用Optional
不要使用类别继续使用另一种与使用Optional
的意图相反的场景。请记住,Optional
表示对象的容器;因此,Optional
添加了另一个抽象级别。换句话说,Optional
的不当使用只是增加了额外的样板代码。
检查Optional
的以下用例,可以看出这一点(此代码违反了前面的“不要对字段使用Optional
”一节):
// 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
来修复此代码,如下所示:
// 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()
方法):
// 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
来修复此代码,如下所示:
// 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
消耗裸引用内存的四倍!
作为结论,在执行以下操作之前,请三思而后行:
// 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
参数的检查来污染前面的代码,这真是个坏主意:
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
类来修复此代码,如下所示:
// 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
:
Book book = new Book(); // Prefer book.renderBook(new Format(), new CoolRenderer(), null);
当一个方法可以接受可选参数时,依赖于旧式方法重载,而不是依赖于Optional
。
240 不要使用Optional
返回空的或null
集合或数组
此外,在不要使用类别中,让我们来讨论如何使用Optional
作为包装空集合或null
集合或数组的返回类型。
返回包装空集合/数组的Optional
或null
集合/数组可能由干净的轻量级代码组成。查看以下代码:
// 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]
)。这是更好的解决方案:
// 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
不是免费的;它只是另一个消耗内存的对象,需要收集。
所以,让我们考虑一个应该避免的解决方案:
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()
如下:
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
来实现琐碎的实现 - JDK8
computeIfAbsent()
方法 - ApacheCommon
DefaultedMap
我们可以得出结论,总有比在集合中使用Optional
更好的解决方案。
但是前面讨论的用例并不是最坏的场景。这里还有两个必须避免的问题:
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
:
// 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()
,如下所示:
// 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()
不是灾难,但可能会造成一些混乱,没有任何价值。检查以下代码:
// 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()
,如下所示:
// Avoid Optional<String> result = Optional.of(str) .filter(not(String::isEmpty));
但是请记住,String
可以是null
。此解决方案适用于空字符串或非空字符串,但不适用于null
字符串。因此,ofNullable()
给出了合适的解决方案,如下:
// Prefer Optional<String> result = Optional.ofNullable(str) .filter(not(String::isEmpty));
243 Optional<T>
与OptionalInt
如果没有使用装箱基本类型的具体原因,则宜避免Optional<T>
并依赖非通用OptionalInt
、OptionalLong
或OptionalDouble
型。
装箱和拆箱是昂贵的操作,容易导致性能损失。为了消除这种风险,我们可以依赖于OptionalInt
、OptionalLong
和OptionalDouble
。这些是int
、long
和double
原始类型的包装器。
因此,请避免使用以下(及类似)解决方案:
// Avoid Optional<Integer> priceInt = Optional.of(50); Optional<Long> priceLong = Optional.of(50L); Optional<Double> priceDouble = Optional.of(49.99d);
更喜欢以下解决方案:
// 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()
的源代码:
@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
对象:
Optional<String> actual = ...; Optional<String> expected = ...; // or Optional actual = ...; Optional expected = ...;
建议避免进行以下测试:
// Avoid @Test public void givenOptionalsWhenTestEqualityThenTrue() throws Exception { assertEquals(expected.get(), actual.get()); }
如果预期和/或实际为空,get()
方法将导致NoSuchElementException
类型的异常。
最好使用以下测试:
// Prefer @Test public void givenOptionalsWhenTestEqualityThenTrue() throws Exception { assertEquals(expected, actual); }
245 通过map()
和flatMap()
转换值
Optional.map()
和flatMap()
方法便于转换Optional
值。
map()
方法将函数参数应用于值,然后返回包装在Optional
对象中的结果。flatMap()
方法将函数参数应用于值,然后直接返回结果。
假设我们有Optional<String>
,我们想把这个String
从小写转换成大写。一个没有灵感的解决方案可以写如下:
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()
,如下所示:
// Prefer Optional<String> uppername = lowername.map(String::toUpperCase);
map()
方法也可以用来避免破坏 Lambda 链。让我们考虑一下List<Book>
,我们想找到第一本便宜 50 美元的书,如果有这样一本书,就把它的书名改成大写。同样,没有灵感的解决方案如下:
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 链来实现:
// Prefer String title = books.stream() .filter(b -> b.getPrice()<50) .findFirst() .map(Book::getTitle) .map(String::toUpperCase) .orElse(NOT_FOUND);
在前面的示例中,getTitle()
方法是一个经典的获取器,它将书名返回为String
。但是让我们修改这个获取器以返回Optional
:
public Optional<String> getTitle() { return ...; }
这次我们不能使用map()
,因为map(Book::getTitle)
会返回Optional<Optional<String>>
而不是Optional<String>
。但是如果我们依赖于flatMap()
,那么它的返回将不会被包装在一个额外的Optional
对象中:
// 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:
// Avoid public boolean validateIsbnLength(Book book) { Optional<String> isbn = book.getIsbn(); if (isbn.isPresent()) { return isbn.get().length() > 10; } return false; }
前面的解决方案依赖于显式地展开Optional
值。但是如果我们依赖于Optional.filter()
,我们可以不使用这种显式展开,如下所示:
// 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
。
当我们必须链接Optional
和Stream
API 时,这非常有用。Optional.stream()
方法返回一个元素的Stream
(Optional
的值)或空的Stream
(如果Optional
没有值)。此外,我们可以使用Stream
API 中提供的所有方法。
假设我们有一个按 ISBN 取书的方法(如果没有书与给定的 ISBN 匹配,那么这个方法返回一个空的Optional
对象):
public Optional<Book> fetchBookByIsbn(String isbn) { // fetching book by the given "isbn" can return null Book book = ...; return Optional.ofNullable(book); }
除此之外,我们循环一个 ISBN 的List
,返回Book
的List
如下(每个 ISBN 通过fetchBookByIsbn()
方法传递):
// Avoid public List<Book> fetchBooks(List<String> isbns) { return isbns.stream() .map(this::fetchBookByIsbn) .filter(Optional::isPresent) .map(Optional::get) .collect(toList()); }
这里的重点是以下两行代码:
.filter(Optional::isPresent) .map(Optional::get)
因为fetchBookByIsbn()
方法可以返回空的Optional
类,所以我们必须确保从最终结果中消除它们。为此,我们调用Stream.filter()
并将Optional.isPresent()
函数应用于fetchBookByIsbn()
返回的每个Optional
对象。所以,在过滤之后,我们只有Optional
类具有当前值。此外,我们应用Stream.map()
方法将这些Optional
类解包到Book
。最后,我们收集List
中的Book
对象。
但我们可以更优雅地使用Optional.stream()
来完成同样的事情,具体如下:
// 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
,如下所示:
public static<T> List<T> optionalToList(Optional<T> optional) { return optional.stream().collect(toList()); }
248 Optional
和身份敏感操作
身份敏感操作包括引用相等(==
)、基于身份哈希或同步。
Optional
类是基于值的类,如LocalDateTime
,因此应该避免身份敏感操作。
例如,让我们通过==
测试两个Optional
类的相等性:
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 ==)"); }
这将产生以下输出:
op1 is not equal with op2, (via ==)
因为op1
和op2
不是对同一个对象的引用,所以它们不相等,所以不符合==
的实现。
为了比较这些值,我们需要依赖于equals()
,如下所示:
// 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())"); }
这将产生以下输出:
op1 is equal with op2, (via equals())
在identity-sensitive
操作的上下文中,千万不要这样做(认为Optional
是一个基于值的类,这样的类不应该用于锁定更多细节,请参见这个页面:
Optional<Book> book = Optional.of(new Book()); synchronized(book) { ... }
249 如果Optional
类为空,则返回布尔值
假设我们有以下简单的方法:
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()
实现这个方法,如下所示:
// Avoid (after JDK11) public static boolean cartIsEmpty(long id) { Optional<Cart> cart = fetchCart(id); return !cart.isPresent(); }
这个解决方案可以很好地工作,但不是很有表现力。我们通过存在来检查空虚,我们必须否定isPresent()
的结果。
自 JDK11 以来,Optional
类被一个名为isEmpty()
的新方法所丰富。顾名思义,这是一个标志方法,如果测试的Optional
类为空,它将返回true
。因此,我们可以通过以下方式提高解决方案的表达能力:
// Prefer (after JDK11) public static boolean cartIsEmpty(long id) { Optional<Cart> cart = fetchCart(id); return cart.isEmpty(); }
总结
完成!这是本章的最后一个问题。此时,您应该拥有正确使用Optional
所需的所有参数
从本章下载应用以查看结果和其他详细信息。