第五十五条:谨慎返回Optional

在Java8之前,要编写一个在特定环境下无法返回任何值的方法时,有两种方法:要么抛出异常,要么返回null(假设返回类型是一个对象引用类型)。但这两种方法都不够完美。异常因该根据条件保留起来(详见第69条)。由于创建异常时会捕捉整个堆栈轨迹,因此抛出异常的开销很高。返回null没有这些缺点,但它有自身的不足。如果方法返回null,客户端就必须包含特殊的代码来处理返回null的可能性,除非程序员能证明不可能返回null。如果客户端疏忽了,没有检查null返回值,并将null返回值保存在某个数据结构中,那么未来与这个问题毫不相关的某处代码中,随时有可能发生NullPointerException异常。

在Java8中,还有第三种方法可以编写不能返回值的方法。Optional<T>类代表的是一个不可变的容器,它可以存放单个非null的T引用,或者什么内容都没有。不包含任何内容的optional称为空(empty)。非空的optional中的值称作存在(present)。optional本质上是一个不可变的集合,最多只能存放一个元素。Optional<T>没有实现Collection<T>接口,但原则上是可以的。

理论上能返回T的方法,实践中也可能无法返回,因此在某些特定的条件下,可以改为声明返回Optional<T>。它允许方法返回空的结果,表明无法返回有效的结果。返回Optional的方法比抛出异常的方法使用起来更灵活,也更容易,并比返回null的方法更不容易出错。

在第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。在第30条中说过,更好的替代方法是返回Optional<E>。下面就是修改之后的代码:

// 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:Optional.empty()返回一个空的optional,Optional.of(value)返回一个包含了指定非null值得optional。将null传入Optional.of(value)是一个编程错误。如果这么做,该方法将会抛出NullPointerException。Optional.ofNullable(value)方法接受可能为null的值,当传入null值时就返回一个空的optional。永远不要通过返回Optional的方法返回null:因为它彻底违背了optional的本意。

Strema的许多终止操作都返回optional。如果重新用stream编写max方法,让stream的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());
}

那么,如何选择是返回optional,还是返回null,或者抛出异常呢?Optional本质上与受检异常(详见第71条)相类似,因为它们强迫API用户面对没有返回值的现实。抛出未受检的异常,或者返回null,都允许用户忽略这种可能性,从而可能带来灾难性的后果。但是,抛出受检异常需要在客户端添加额外的样板代码。

如果方法返回optional,客户端必须做出选择:如果该方法不能返回值时应该采取什么动作。你可以指定一个缺省值

// 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);

如果你能够证明optional为非空,就不必指定如果optional为空要采取什么动作,直接从optional获得值即可;但是如果你的判断错了,代码就会抛出一个NosuchElementException

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

有时候,获取缺省值的开销可能很高,除非十分必要,否则还是希望能够避免这一开销。对于这类情况,Optional提供了一个带有Supplier<T>的方法,只在必要的时候才调用它。这个方法叫orElseGet,但或许应该叫orElseCompute,因为它与三个名称以compute开头的Map方法密切相关。有几个Optional方法可以用来处理更加特殊用例的情况:filter、map、flatMap和ifPresent。Java9又在其中新增了两个方法or和ifPresentOrElse。如果上述基本方法不适用,可以查看文档寻找更高级的方法,看看它们是否能够完成你所需的任务。

万一这些方法都无法满足需求,Optional还提供了isPresent()方法,它可以被当作是一个安全阀。当optional中包含一个值时,它返回true;当optional为空时,返回false。该方法可用于对optional结果执行任意的处理,但是要确保正确使用。isPresent的许多用法都可以用上述任意一种方法取代。这样得到的代码一般会骨架简短、清晰,也更符合用法。

例如,以下代码片段打印出一个进程的父进程ID,当该进程没有父进程时打印N/A。这里使用了在Java9中引入的ProcessHand类:

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编程时,经常会遇到Stream<Optional<T>>,为了推动进程还需要一个包含了非空optional中所有元素的Stream<T>。如果使用的是Java8版本,可以像这样弥补差距:

streamOfOptionals.filter(Optional::isPresent).map(Optional::get)

在Java9中,Optional还配有一个stream()方法。这个方法是一个适配器,如果optional中有一个值,它就将Optional变成包含一个元素的Stream;如果optional为空,则其中不包含任何元素。这个方法结果Stream的flatMap方法(详见第45条),可以简洁的取代上述代码片段,如下:

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有两级包装,不是0级。因此,类库设计师认为必须为基本类型int、long和double提供类似Optional<T>的方法。这些optional类型为:OptionalInt、OptionalLong和OptionalDouble。这些包含了Optionl<T>中大部分但并非全部方法。因此,永远不应该返回基本包装类型的optional小型的基本类型(Boolean、Byte、Character、Short和Float)除外

到目前为止,我们已经讨论了返回optional,以及返回之后对它们的处理方法。之所以好没有讨论到其他可能的途径,是因为optional的大部分其他用途都还受到质疑。例如,永远不应该用optional作为映射值。如果这么做,有两种方式来表达一个键的逻辑缺失:要么这个键可以不出现在映射中,要么它可以存在,并映射到一个空的optional。这些即增加了无谓的复杂度,并集有可能造成混淆和出错。更通俗的说,几乎永远都不适合用optional作为键、值,或者集合或数组中的元素

这里留下了一个尚未解答的问题:适合将optional保存在实例中吗?这个答案散发着恶臭的气息:它建议使用包含optional域的的子类。不过有时候它又是有道理的。以第2条中的NutritionFacts类为例,NutritionFacts实例中包含了许多不必要的域。你不可能给这些域中每一个可能的合并都提供一个子类。而且,这些域有基本类型,导致不方便直接描述这种缺失。NutritionFacts最好的API会从get方法处为每个optional域获得一个optional,因此将那些optional作为域保存在对象中的做法变得很有意义

总而言之,如果发现子自己在编写的方法始终无法返回值,并且相信该方法的用户每次在调用它时都要考虑到这种可能性,那么或许就应该返回一个optional。但是,应当注意到与返回optional相关的真实的性能影响;对于注重性能的方法,最好是返回一个null,或者抛出异常。最后,尽量不要将optional用作返回值以外的任何其他用途。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容