effective-java-3rd-chinese/docs/notes/55. 明智审慎地返回 Optional.md
2019-03-20 14:17:11 +08:00

121 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 55. 明智审慎地返回 Optional
  在 Java 8 之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回 null(假设返回类型是对象是引用类型)。但这两种方法都不完美。应该为异常条件保留异常 (条目 69),并且抛出异常代价很高,因为在创建异常时捕获整个堆栈跟踪。返回 null 没有这些缺点,但是它有自己的缺陷。如果方法返回 null客户端必须包含特殊情况代码来处理 null 返回的可能性,除非程序员能够证明 null 返回是不可能的。如果客户端忽略检查 null 返回并将 null 返回值存储在某个数据结构中,那么会在将来的某个时间在与这个问题不相关的代码位置上,抛出`NullPointerException`异常的可能性。
  在 Java 8 中,还有第三种方法来编写可能无法返回任何值的方法。`Optional<T>`类表示一个不可变的容器,它可以包含一个非 null 的`T`引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空empty。非空的包含值称的 Optional 被称为存在present。Optional 的本质上是一个不可变的集合,最多可以容纳一个元素。`Optional<T>`没有实现`Collection<T>`接口,但原则上是可以。
  在概念上返回 T 的方法,但在某些情况下可能无法这样做,可以声明为返回一个`Optional<T>`。这允许该方法返回一个空结果,以表明不能返回有效的结果。返回 Optional 的方法比抛出异常的方法更灵活、更容易使用,而且比返回 null 的方法更不容易出错。
  在条目 30 中,我们展示了根据集合中元素的自然顺序计算集合最大值的方法。
```java
// Returns maximum value in collection - throws exception if empty
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("Empty collection");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
```
  如果给定集合为空,此方法将抛出`IllegalArgumentException`异常。我们在条目 30 中提到,更好的替代方法是返回`Optional<E>`。下面是修改后的方法:
```java
// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
if (c.isEmpty())
return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
```
  如你所见,返回 Optional 很简单。 你所要做的就是使用适当的静态工厂创建 Optional。 在这个程序中,我们使用两个:`Optional.empty()` 返回一个空的 Optional `Optional.of(value)` 返回一个包含给定非 null 值的 Optional。 将 null 传递给 `Optional.of(value)` 是一个编程错误。 如果这样做,该方法通过抛出 `NullPointerException` 异常作为回应。 `Optional.of(value)` 方法接受一个可能为 null 的值,如果传入 null 则返回一个空的 Optional。**永远不要通过返回 Optional 的方法返回一个空值**:它破坏 Optional 设计的初衷。
  `Stream` 上的很多终止操作返回 Optional。如果我们重写 max 方法来使用一个`Stream`,那么 `Stream``max` 操作会为我们生成 Optional 的工作 (尽管我们还是传递一个显式的`Comparator`):
```java
// Returns max val in collection as Optional<E> - uses stream
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
```
  那么,如何选择返回 Optional 而不是返回 null 或抛出异常呢?`Optional`在本质上类似于检查异常checked exceptions(条目 71),因为它们迫使 API 的用户面对可能没有返回任何值的事实。抛出未检查的异常或返回 null 允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端中添加额外的样板代码。
  如果方法返回一个 Optional则客户端可以选择在方法无法返回值时要采取的操作。 可以指定默认值:
```java
// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");
```
  或者可以抛出任何适当的异常。注意,我们传递的是异常工厂,而不是实际的异常。这避免了创建异常的开销,除非它真的实际被抛出:
```java
// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
```
  如果你能证明 Optional 非空,你可以从 Optional 获取值,而不需要指定一个操作来执行。但是如果 Optional 是空的,你判断错了,代码会抛出一个 `NoSuchElementException` 异常:
```java
// Using optional when you know theres a return value
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
```
  有时候可能会遇到这样一种情况获取默认值的代价很高除非必要否则希望避免这种代价。对于这些情况Optional 提供了一个方法,该方法接受 `Supplier<T>`,并仅在必要时调用它。这个方法被称为 `orElseGet`,但是或许应该被称为 `orElseCompute`,因为它与以`compute`开头的三个 Map 方法密切相关。有几个 Optional 的方法来处理更特殊的用例:`filter`、`map` 、`flatMap` 和 `ifPresent`。在 Java 9 中,又添加了两个这样的方法: `or``ifPresentOrElse`。如果上面描述的基本方法与你的用例不太匹配,请查看这些更高级方法的文档,并查看它们是否能够完成任务。
  如果这些方法都不能满足你的需要Optional 提供 `isPresent()` 方法,可以将其视为安全阀。如果 Optional 包含值,则返回 true如果为空则返回 false。你可以使用此方法对可选结果执行任何喜欢的处理但请确保明智地使用它。`isPresent` 的许多用途都可以被上面提到的一种方法所替代。生成的代码通常更短、更清晰、更符合习惯。
  例如,请考虑此代码段,它打印一个进程的父进程 ID如果进程没有父进程则打印 N/A. 该代码段使用 Java 9 中引入的 `ProcessHandle` 类:
```java
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
String.valueOf(parentProcess.get().pid()) : "N/A"));
```
  上面的代码可以被如下代码所替代,使用了 Optional 的 `map` 方法:
```java
System.out.println("Parent PID: " +
ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
```
  当使用 Stream 进行编程时,通常会发现使用的是一个 `Stream<Optional<T>>`,并且需要一个 `Stream<T>`,其中包含非 Optional 中的所有元素,以便继续进行。如果你正在使用 Java 8下面是弥补这个差距的代码:
```java
streamOfOptionals
.filter(Optional::isPresent)
.map(Optional::get)
```
  在 Java 9 中Optional 配备了一个 `stream()` 方法。这个方法是一个适配器, 此方法是一个适配器,它将 Optional 变为包含一个元素的 Stream如果 Optional 为空,则不包含任何元素。此方法与 Stream 的 `flatMap` 方法 (条目 45) 相结合,这个方法可以简洁地替代上面的方法:
```java
streamOfOptionals.
.flatMap(Optional::stream)
```
  并不是所有的返回类型都能从 Optional 的处理中获益。**容器类型包括集合、映射、Stream、数组和 Optional不应该封装在 Optional 中**。与其返回一个空的`Optional<List<T>>`,不还如返回一个空的 `List<T>`(条目 54)。返回空容器将消除客户端代码处理 Optional 的需要。`ProcessHandle` 类确实有 `arguments` 方法,它返回`Optional<String[]>`,但是这个方法应该被视为一种异常,不该被效仿。
  那么什么时候应该声明一个方法来返回 `Optional <T>` 而不是 `T` 呢? 通常,**如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回 Optional <T> 的方法**。也就是说,返回 `Optional <T>` 并非没有成本。 Optional 是必须分配和初始化的对象,从 Optional 中读取值需要额外的迂回。 这使得 Optional 不适合在某些性能关键的情况下使用。 特定方法是否属于此类别只能通过仔细测量来确定(详见第 67 条)。
  与返回装箱的基本类型相比,返回包含已装箱基本类型的 Optional 的代价高得惊人,因为 Optional 有两个装箱级别,而不是零。因此,类库设计人员认为为基本类型 int、long 和 double 提供类似 `Option<T>` 是合适的。这些 Option 是 `OptionalInt`、`OptionalLong` 和 `OptionalDouble`。它们包含 `Optional<T>` 上的大多数方法但不是所有方法。因此除了“次要基本类型minor primitive types”BooleanByteCharacterShort 和 Float 之外,**永远不应该返回装箱的基本类型的 Optional**。
  到目前为止,我们已经讨论了返回 Optional 并在返回后处理它们的方法。我们还没有讨论其他可能的用法,这是因为大多数其他 Optional 的用法都是可疑的。例如,永远不要将 Optional 用作映射值。如果这样做则有两种方法可以表示键key在映射中逻辑上的缺失键要么不在映射中要么存在的话映射到一个空的 Optional。这反映了不必要的复杂性很有可能导致混淆和错误。更通俗地说在集合或数组中使用 Optional 的键、值或元素几乎都是不合适的。
  这里留下了一个悬而未决的大问题。在实例中存储 Optional 属性是否合适吗?通常这是一种“不好的味道”:它建议你可能应该有一个包含 Optional 属性的子类。但有时这可能是合理的。考虑条目 2 中的 `NutritionFacts` 类的情况。`NutritionFacts` 实例包含许多不需要的属性。不可能为这些属性的每个可能组合都提供一个子类。此外,属性包含基本类型,这使得很难直接表示这种缺失。对于 `NutritionFacts` 最好的 API 将为每个 Optional 属性从 getter 方法返回一个 Optional因此将这些 Optional 作为属性存储在对象中是很有意义的。
  总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。