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 中,我们展示了根据集合中元素的自然顺序计算集合最大值的方法。

  1. // Returns maximum value in collection - throws exception if empty
  2. public static <E extends Comparable<E>> E max(Collection<E> c) {
  3. if (c.isEmpty())
  4. throw new IllegalArgumentException("Empty collection");
  5. E result = null;
  6. for (E e : c)
  7. if (result == null || e.compareTo(result) > 0)
  8. result = Objects.requireNonNull(e);
  9. return result;
  10. }

  如果给定集合为空,此方法将抛出IllegalArgumentException异常。我们在条目 30 中提到,更好的替代方法是返回Optional<E>。下面是修改后的方法:

  1. // Returns maximum value in collection as an Optional<E>
  2. public static <E extends Comparable<E>>
  3. Optional<E> max(Collection<E> c) {
  4. if (c.isEmpty())
  5. return Optional.empty();
  6. E result = null;
  7. for (E e : c)
  8. if (result == null || e.compareTo(result) > 0)
  9. result = Objects.requireNonNull(e);
  10. return Optional.of(result);
  11. }

  如你所见,返回 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,那么 Streammax 操作会为我们生成 Optional 的工作 (尽管我们还是传递一个显式的Comparator):

  1. // Returns max val in collection as Optional<E> - uses stream
  2. public static <E extends Comparable<E>>
  3. Optional<E> max(Collection<E> c) {
  4. return c.stream().max(Comparator.naturalOrder());
  5. }

  那么,如何选择返回 Optional 而不是返回 null 或抛出异常呢?Optional在本质上类似于检查异常(checked exceptions)(详见第 71 条),因为它们迫使 API 的用户面对可能没有返回任何值的事实。抛出未检查的异常或返回 null 允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端中添加额外的样板代码。

  如果方法返回一个 Optional,则客户端可以选择在方法无法返回值时要采取的操作。 可以指定默认值:

  1. // Using an optional to provide a chosen default value
  2. String lastWordInLexicon = max(words).orElse("No words...");

  或者可以抛出任何适当的异常。注意,我们传递的是异常工厂,而不是实际的异常。这避免了创建异常的开销,除非它真的实际被抛出:

  1. // Using an optional to throw a chosen exception
  2. Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

  如果你能证明 Optional 非空,你可以从 Optional 获取值,而不需要指定一个操作来执行。但是如果 Optional 是空的,你判断错了,代码会抛出一个 NoSuchElementException 异常:

  1. // Using optional when you know there’s a return value
  2. Element lastNobleGas = max(Elements.NOBLE_GASES).get();

  有时候,可能会遇到这样一种情况:获取默认值的代价很高,除非必要,否则希望避免这种代价。对于这些情况,Optional 提供了一个方法,该方法接受 Supplier<T>,并仅在必要时调用它。这个方法被称为 orElseGet,但是或许应该被称为 orElseCompute,因为它与以compute开头的三个 Map 方法密切相关。有几个 Optional 的方法来处理更特殊的用例:filtermapflatMapifPresent。在 Java 9 中,又添加了两个这样的方法: orifPresentOrElse。如果上面描述的基本方法与你的用例不太匹配,请查看这些更高级方法的文档,并查看它们是否能够完成任务。

  如果这些方法都不能满足你的需要,Optional 提供 isPresent() 方法,可以将其视为安全阀。如果 Optional 包含值,则返回 true;如果为空,则返回 false。你可以使用此方法对可选结果执行任何喜欢的处理,但请确保明智地使用它。isPresent 的许多用途都可以被上面提到的一种方法所替代。生成的代码通常更短、更清晰、更符合习惯。

  例如,请考虑此代码段,它打印一个进程的父进程 ID,如果进程没有父进程,则打印 N/A. 该代码段使用 Java 9 中引入的 ProcessHandle 类:

  1. Optional<ProcessHandle> parentProcess = ph.parent();
  2. System.out.println("Parent PID: " + (parentProcess.isPresent() ?
  3. String.valueOf(parentProcess.get().pid()) : "N/A"));

  上面的代码可以被如下代码所替代,使用了 Optional 的 map 方法:

  1. System.out.println("Parent PID: " +
  2. ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

  当使用 Stream 进行编程时,通常会发现使用的是一个 Stream<Optional<T>>,并且需要一个 Stream<T>,其中包含非 Optional 中的所有元素,以便继续进行。如果你正在使用 Java 8,下面是弥补这个差距的代码:

  1. streamOfOptionals
  2. .filter(Optional::isPresent)
  3. .map(Optional::get)

  在 Java 9 中,Optional 配备了一个 stream() 方法。这个方法是一个适配器, 此方法是一个适配器,它将 Optional 变为包含一个元素的 Stream,如果 Optional 为空,则不包含任何元素。此方法与 Stream 的 flatMap 方法 (条目 45) 相结合,这个方法可以简洁地替代上面的方法:

  1. streamOfOptionals.
  2. .flatMap(Optional::stream)

  并不是所有的返回类型都能从 Optional 的处理中获益。容器类型,包括集合、映射、Stream、数组和 Optional,不应该封装在 Optional 中。与其返回一个空的Optional<List<T>>,不还如返回一个空的 List<T>(详见第 54 条)。返回空容器将消除客户端代码处理 Optional 的需要。ProcessHandle 类确实有 arguments 方法,它返回Optional<String[]>,但是这个方法应该被视为一种异常,不该被效仿。

  那么什么时候应该声明一个方法来返回 Optional<T> 而不是 T 呢? 通常,如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回 Optional \ 的方法。也就是说,返回 Optional<T> 并非没有成本。 Optional 是必须分配和初始化的对象,从 Optional 中读取值需要额外的迂回。 这使得 Optional 不适合在某些性能关键的情况下使用。 特定方法是否属于此类别只能通过仔细测量来确定(详见第 67 条)。

  与返回装箱的基本类型相比,返回包含已装箱基本类型的 Optional 的代价高得惊人,因为 Optional 有两个装箱级别,而不是零。因此,类库设计人员认为为基本类型 int、long 和 double 提供类似 Option<T> 是合适的。这些 Option 是 OptionalIntOptionalLongOptionalDouble。它们包含 Optional<T> 上的大多数方法,但不是所有方法。因此,除了「次要基本类型(minor primitive types)」Boolean,Byte,Character,Short 和 Float 之外,永远不应该返回装箱的基本类型的 Optional

  到目前为止,我们已经讨论了返回 Optional 并在返回后处理它们的方法。我们还没有讨论其他可能的用法,这是因为大多数其他 Optional 的用法都是可疑的。例如,永远不要将 Optional 用作映射值。如果这样做,则有两种方法可以表示键(key)在映射中逻辑上的缺失:键要么不在映射中,要么存在的话映射到一个空的 Optional。这反映了不必要的复杂性,很有可能导致混淆和错误。更通俗地说,在集合或数组中使用 Optional 的键、值或元素几乎都是不合适的。

  这里留下了一个悬而未决的大问题。在实例中存储 Optional 属性是否合适吗?通常这是一种“不好的味道”:它建议你可能应该有一个包含 Optional 属性的子类。但有时这可能是合理的。考虑条目 2 中的 NutritionFacts 类的情况。NutritionFacts 实例包含许多不需要的属性。不可能为这些属性的每个可能组合都提供一个子类。此外,属性包含基本类型,这使得很难直接表示这种缺失。对于 NutritionFacts 最好的 API 将为每个 Optional 属性从 getter 方法返回一个 Optional,因此将这些 Optional 作为属性存储在对象中是很有意义的。

  总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。