在Java 8之前,在编写在某些情况下无法返回值的方法时,可以采用两种方法。要么抛出异常,要么返回null(假设返回类型是对象引用类型)。这两种方法都不完美。应该为异常条件保留异常(item69 ),并且抛出异常代价高昂,因为在创建异常时捕获整个堆栈跟踪。返回null没有这些缺点,但是它有自己的缺点。如果方法返回null,客户端必须包含特殊情况代码来处理null返回的可能性,除非程序员能够证明null返回是不可能的。如果客户端忽略检查null返回并将null返回值存储在某个数据结构中,那么NullPointerException可能会在将来的某个时间,在代码中的某个与该问题无关的位置产生。
在Java 8中,还有第三种方法来编写可能无法返回值的方法。Optional<T>类表示一个不可变的容器,它可以包含一个非空的T引用,也可以什么都不包含。不包含任何内容的可选项被称为empty.一个值被认为存在于一个非空的可选值中。一个可选的本质上是一个不可变的集合,它最多可以容纳一个元素。Optional<T>不实现Collection<T>,但原则上可以。
在概念上返回T但在某些情况下可能无法这样做的方法可以声明为返回一个Optional<T>。这允许该方法返回一个空结果,以表明它不能返回有效的结果。 Optional-returning 方法比抛出异常的方法更灵活、更容易使用,而且比返回null的方法更不容易出错。
在第30项中,我们展示了根据集合元素的自然顺序计算集合最大值的方法。
image.png
如果给定集合为空,此方法将抛出IllegalArgumentException。我们在第30项中提到,更好的替代方法是返回Optional<E>。下面是修改后的方法:
image.png
如您所见,返回一个可选的是很简单的。您所要做的就是使用适当的静态工厂创建optional 。在这个程序中,我们使用两个:option .empty()返回一个空的可选值,option .of(value)返回一个包含给定非空值的可选值。将null传递给option .of(value)是一个编程错误。如果您这样做,该方法将通过抛出NullPointerException来响应。ofnullable (value)方法接受一个可能为空的值,如果传入null,则返回一个空的可选值。永远不要从选Optional-returning 方法返回空null:它破坏了整个设施的目的。
许多流上的终端操作返回optionals。如果我们重写max方法来使用一个流,那么流的max操作会为我们生成一个optional(尽管我们必须传递一个显式的比较器):
那么,如何选择返回optional 而不是返回null或抛出异常呢? Optionals 在本质上类似于检查异常(item71 ) 因为它们迫使API的用户面对可能没有返回值的事实。抛出未检查的异常或返回null允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个已检查的异常需要在客户机中添加额外的样板代码。
如果一个方法返回一个optional,客户端可以选择如果该方法不能返回值该采取什么操作。您可以指定一个默认值:
image.png或者您可以抛出任何适当的异常。注意,我们传递的是异常工厂,而不是实际的异常。这避免了创建异常的开销,除非它实际被抛出:
image.png
如果你能证明一个optional 是非空的,你可以从optional 获取值,而不需要指定一个操作来执行,如果optional 是空的,但是如果你错了,你的代码会抛出一个NoSuchElementException:
image.png
有时候,您可能会遇到这样一种情况:获取默认值的代价很高,除非必要,否则您希望避免这种代价。对于这些情况,Optional提供了一个方法,该方法接受Supplier<T>,并仅在必要时调用它。这个方法被称为orElseGet,但是它可能应该被称为orElseCompute,因为它与以compute开头的三个Map方法密切相关。有几个Optional 方法来处理更专业的用例:filter, map, flatMap,和ifPresent. 在Java 9中,又添加了两个这样的方法:or和ifPresentOrElse。如果上面描述的基本方法与您的用例不太匹配,请查看这些更高级方法的文档,并查看它们是否能够完成任务。
如果这些方法都不能满足您的需要,Optional提供isPresent()方法,可以将其视为安全阀。如果可选项包含值,则返回true;如果为空,则返回false。您可以使用此方法对可选结果执行任何您喜欢的处理,但请确保明智地使用它。isPresent的许多用途都可以被上面提到的一种方法所替代。生成的代码通常更短、更清晰、更符合习惯。
例如,考虑这段代码,它打印进程父进程的进程ID,如果进程没有父进程,则打印N/ a。该代码段使用了在Java 9中引入的ProcessHandle类:
上面的代码片段可以替换为这个,它使用了Optional的map函数:
image.png
当使用流进行编程时,通常会发现您使用的是一个Stream<Optional<T>>,并且需要一个Stream<T>,其中包含非空选项中的所有元素,以便继续。如果你正在使用Java 8,下面是如何弥补这个差距的:
image.png
在Java 9中,Optional配备了一个stream()方法。:这个方法是一个适配器,它将一个可选的元素转换成一个包含元素的流(如果一个元素出现在可选的元素中),如果一个元素是空的,则一个元素都没有。与Stream的flatMap方法(第45项)相结合,这个方法为上面的代码段提供了一个简洁的替换:
并不是所有的返回类型都能从optional 处理中获益。容器类型,包括集合、映射、流、数组和选项,不应该封装在选项中。与其返回一个空的可选的<List<T>>,不如返回一个空的List<T>(item54)。返回空容器将消除客户机代码处理可选容器的需要。ProcessHandle类确实有arguments方法,它返回可选的<String[]>,但是这个方法应该被视为一种异常,不能被模拟。
那么,什么时候应该声明一个方法来返回Optional<T>而不是T?通常,您应该声明一个方法来返回Optianal<T>,如果它可能无法返回结果,并且如果没有返回结果,客户机将不得不执行特殊处理。也就是说,返回一个也就是说,返回一个可选的<T>不是没有代价的。<T>不是没有代价的。
Optional 对象是必须分配和初始化的对象,从Optional 对象中读取值需要额外的间接操作。这使得选项不适合在某些性能关键的情况下使用。某一特定方法是否属于这一类只能通过仔细测量来确定( item67 )。
与返回基元类型相比,返回包含已装箱基元类型的可选类型的代价高得惊人,因为可选类型有两个装箱级别,而不是零。因此,库设计人员认为为基本类型int、long和double提供Optional<T>类似物是合适的.这些可选类型是OptionalInt、OptionalLong和OptionalDouble。它们包含Optional<T>上的大多数方法,但不是所有方法。因此,除了“次要基本类型”布尔型、字节型、字符型、Short型和浮点型之外,永远不应该返回装箱的基本类型的可选值。
到目前为止,我们已经讨论了返回选项并在返回后处理它们。我们还没有讨论其他可能的用法,这是因为大多数其他选项的用法都是可疑的.例如,永远不要将选项用作映射值。如果你这样做了,你有两种方式来表达键在map上的逻辑缺失:键可以不在映射中,也可以存在并映射到空的可选项。这代表了不必要的复杂性,很有可能导致混淆和错误。更普遍地,在集合或数组中使用optional 的键、值或元素几乎是不合适的。
这留下了一个悬而未决的大问题。在实例字段中存储optional 字段是否合适?通常这是一种“臭味”:它建议您可能应该有一个包含optional 字段的子类。但有时这可能是合理的。考虑第2项中我们的optional 类的情况。NutritionFacts实例包含许多不需要的字段。不能为这些字段的每个可能组合都有子类。此外,字段具有原始类型,这使得直接表示缺席非常困难。:对于NutritionFacts最好的API将为每个optional 字段从getter返回一个optional ,因此将这些选项作为字段存储在对象中是很有意义的。
总之,如果您发现自己编写的方法不能总是返回值,并且您认为该方法的用户在每次调用时考虑这种可能性很重要,那么您可能应该返回一个可选的方法。但是,您应该意识到,返回选项会带来实际的性能后果;对于性能关键的方法,最好返回null或抛出异常。最后,除了作为返回值之外,您不应该在任何其他容量中使用optional 。
本文写于2019.7.18,历时1天
网友评论