ITEM 55: RETURN OPTIONALS JUDICIOUSLY
在Java 8之前,在编写在某些情况下无法返回值的方法时,可以采用两种方法。可以抛出异常,也可以返回 null (假设返回类型是对象引用类型)。
这两种方法都不是完美的。应该为异常条件保留异常(item 69),并且抛出异常代价高昂,因为在创建异常时捕获整个堆栈跟踪。
返回null没有这些缺点,但是它有自己的缺点。如果一个方法返回null,客户端必须包含特殊情况的代码来处理null返回的可能性,除非程序员能够证明null返回是不可能的。如果客户端忽略了检查空返回并将空返回值存储在某个数据结构中,那么NullPointerException 可能会在将来的某个时候出现,在代码的某个地方,而这个地方与问题无关。
在 Java 8 中,还有第三种方法来编写可能无法返回值的方法。可选的类表示一个不可变的容器,它可以包含一个非空的 T 引用,也可以什么都不包含。不包含任何内容的可选项被认为是空的。一个值被认为存在于一个非空的可选项中。一个可选的本质上是一个不可变的集合,它最多可以容纳一个元素。可选的不实现集合,但原则上可以。
在概念上返回 T 但在某些情况下可能无法这样做的方法可以声明为返回 Optional<T>。这允许该方法返回一个空结果,以表明它不能返回有效的结果。与抛出异常的方法相比,返回选项的方法更灵活、更容易使用,而且比返回null的方法更少出错。
在item 30 中,我们展示了根据集合元素的自然顺序计算集合最大值的方法。
// 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。我们在 item 30 中提到,更好的替代方法是返回 Optional<T>。下面是修改后的方法:
// 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 。在这个程序中,我们使用了 option.empty() 返回一个空的可选值,以及 option.of(value) 返回一个包含给定非空值的可选值。将 null 传递给 option.of(value) 是一个编程错误。如果这样做,该方法将通过抛出 NullPointerException 来响应。
Optional.ofNullable(value) 方法接受一个可能为空的值,如果传入 null,则返回一个空的可选值。永远不要从选项返回方法返回空值:这违背了该工具的全部用途。
许多流上的终端操作返回选项。如果我们重写 max 方法来使用一个流,流的 max 操作为我们生成了一个 Optional (尽管我们必须通过一个显式的比较器):
// 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());
}
那么如何选择返回可选的而不是返回 null 或抛出异常呢?选项在本质上类似于受控异常(item 71),因为它们迫使 API 的用户面对可能没有返回值的事实。抛出未检查的异常或返回空值允许用户忽略这种可能性,从而产生潜在的可怕后果。但是,抛出已检查的异常需要在客户机中添加额外的样板代码。
如果一个方法返回一个可选的值,那么客户端可以选择在方法不能返回值时采取什么操作。你可以指定一个默认值:
// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");
或者您可以抛出任何适当的异常。注意,我们传递的是一个异常工厂,而不是一个实际的异常。这避免了创建异常的开销,除非它真的被抛出:
// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
如果你能证明一个可选的是非空的,你可以从可选的获取值,而不需要在可选的是空的时候指定一个动作,但是如果你错了,你的代码会抛出一个NoSuchElementException:
// Using optional when you know there’s a return value
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
有时候,您可能会遇到这样一种情况:获取默认值的代价很高,除非必要,否则您希望避免这种代价。对于这些情况,Optional 提供了一个方法,该方法接受 Supplier,只在必要时调用它。这个方法被称为 orElseGet,但是也许它应该被称为orElseCompute,因为它与以 compute 开头的三个 Map 方法密切相关。处理更特殊的用例有几个可选的方法:filter、map、flatMap 和 ifPresent。在 Java 9 中,又添加了两个这样的方法:or 和 ifPresentOrElse。如果上面描述的基本方法与您的用例不匹配,请查看这些更高级方法的文档,并查看它们是否能完成任务。
如果这些方法都不能满足您的需要,Optional 提供 isPresent() 方法,可以将其视为安全阀。如果可选项包含一个值,则返回true;如果为空,则返回false。您可以使用此方法对可选结果执行任何您喜欢的处理,但请确保明智地使用它。isPresent 的许多用途都可以用上面提到的一种方法来代替。结果代码通常会更短、更清晰、更习惯。
例如,考虑这个代码片段,它打印进程父进程的进程 ID,或者 N/Aif 进程没有父进程。代码段使用了在 Java 9 中引入的 ProcessHandle 类:
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
String.valueOf(parentProcess.get().pid())
: "N/A"));
上面的代码片段可以替换为这个,它使用了Optional的map函数:
System.out.println("Parent PID: " + ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
在使用流进行编程时,经常会遇到这样的情况: Stream<Optional<T>> 需要一个 Stream<T> 包含非空选项中的所有元素的流才能继续。如果你正在使用 Java 8,以下是如何弥补差距:
streamOfOptionals.filter(Optional::isPresent).map(Optional::get)
在Java 9中,Optional 配备了一个 stream() 方法。此方法是一个适配器,它将可选的元素转换为包含元素的流(如果在可选元素中存在元素),或者将空元素转换为空元素。结合 Stream 的 flatMap 方法(item 45),这个方法提供了一个简洁的替代上面的代码片段:
streamOfOptionals.flatMap(Optional::stream)
并不是所有的返回类型都能从可选的处理中受益。容器类型,包括 collections, maps, streams, arrays, and optionals,不应该封装在选项中。与其返回一个空的 Optional<List<T>>,不如返回一个空的 List<T> (item 54)。返回空容器将消除客户端代码处理可选对象的需要。ProcessHandle 类确实有 arguments 方法,它返回Optional<String[]> ,但是这个方法应该被视为一种异常,不能被模拟。
那么,什么时候应该声明一个方法来返回 Optional<T> 而不是 T ?通常,如果方法可能无法返回结果,那么应该声明一个方法来返回可选的,如果没有返回结果,客户机将不得不执行特殊的处理。也就是说,返回一个可选的不是没有代价的。可选对象是必须分配和初始化的对象,从可选对象中读取值需要额外的间接操作。这使得选项不适合在某些性能关键的情况下使用。某一特定方法是否属于这一类只能通过仔细测量来确定(item 67)。
与返回原语类型相比,返回包含已装箱原语类型的可选类型的开销非常大,因为可选类型有两个装箱级别,而不是零。因此,库设计人员认为应该为基本类型 int、long 和 double 提供类似的可选。这些可选类型是 OptionalInt、OptionalLong 和OptionalDouble。它们包含了可选的上的大部分方法,但不是全部。
因此,除了“次要基本类型(minor primitive types)Boolean, Byte, Character, Short, and Float 之外,您不应该返回装箱的基本类型的可选值。
到目前为止,我们已经讨论了返回选项并在返回后处理它们。我们还没有讨论其他可能的用法,这是因为大多数其他的选项用法都是可疑的。
例如,您不应该使用 optionals 作为映射值。如果这样做,就有两种方法来表示键在映射中的逻辑缺失:要么键在映射中不存在,要么它存在并映射到一个空的可选项。这代表了不必要的复杂性和巨大的混乱和错误的潜力。更一般地说,在集合或数组中使用可选的键、值或元素几乎是不合适的。
这就留下了一个悬而未决的大问题。在实例字段中存储可选字段是否合适?通常这是一种“不好的味道”:它建议您可能应该有一个包含可选字段的子类。但有时它可能是合理的。请考虑 item 2 中我们的 NutritionFacts 类的情况。一个 NutritionFacts 实例包含许多不需要的字段。不可能对这些字段的每个可能组合都有子类。此外,字段具有原始类型,这使得直接表示缺憾非常困难。对于 NutritionFacts,最好的 API 将为每个可选字段从 getter 返回一个可选的,所以简单地将这些选项作为字段存储在对象中是很有意义的。
总之,如果您发现自己编写的方法不能总是返回值,并且您认为该方法的用户在每次调用它时考虑这种可能性是很重要的,那么您可能应该返回一个可选的。但是,您应该意识到,返回选项会带来实际的性能后果;对于性能关键型方法,最好是返回 null或抛出异常。最后,除了作为返回值之外,您不应该在任何其他容量中使用optional。