11.4 使用Optional的实战示例

相信你已经了解,有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。

实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,那么很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional带来的威力。我们会通过几个实际的例子讲解如何达到这样的目的。

11.4.1 用Optional封装可能为null的值

现存Java API几乎都是通过返回一个null的方式来表示需要值的缺失,或者由于某些原因计算无法得到该值。比如,如果Map中不含指定的键对应的值,它的get方法就会返回一个null。但是,正如之前介绍的,大多数情况下,你可能希望这些方法能返回一个Optional对象。你无法修改这些方法的签名,但是你很容易用Optional对这些方法的返回值进行封装。我们接着用Map做例子,假设你有一个Map方法,访问由key索引的值时,如果map中没有与key关联的值,该次调用就会返回一个null

  1. Object value = map.get("key");

使用Optional封装map的返回值,可以对这段代码进行优化。要达到这个目的有两种方式:使用笨拙的if-then-else判断语句,毫无疑问这种方式会增加代码的复杂度;或者采用前文介绍的Optional.ofNullable方法:

  1. Optional<Object> value = Optional.ofNullable(map.get("key"));

每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考虑使用这种方法。

11.4.2 异常与Optional的对比

由于某种原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是抛出一个异常。这种情况比较典型的例子是使用静态方法Integer.parseInt(String),将String转换为int。在这个例子中,如果String无法解析到对应的整型,该方法就抛出一个NumberFormatException。最后的效果是,发生String无法转换为int时,代码发出一个遭遇非法参数的信号,唯一的不同是,这次你需要使用try/catch语句,而不是使用if条件判断来控制一个变量的值是否非空。

你也可以用空的Optional对象,对遭遇无法转换的String时返回的非法值进行建模,这时你期望parseInt的返回值是一个Optional。我们无法修改最初的Java方法,但是这无碍我们进行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的Optional对象,代码如下所示。

代码清单 11-7 将String转换为Integer,并返回一个Optional对象

  1. public static Optional<Integer> stringToInt(String s) {
  2. try {
  3. return Optional.of(Integer.parseInt(s)); ←---- 如果String能转换为对应的Integer,将其封装在Optional对象中返回
  4. } catch (NumberFormatException e) {
  5. return Optional.empty(); ←---- 否则返回一个空的Optional对象
  6. }
  7. }

我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用OptionalUtility.stringToInt方法,将String转换为一个Optional对象,而不再需要记得你在其中封装了笨拙的try/catch的逻辑了。

11.4.3 基础类型的Optional对象,以及为什么应该避免使用它们

不知道你注意到了没有,与Stream对象一样,Optional也提供了类似的基础类型—— OptionalIntOptionalLong以及OptionalDouble——所以代码清单11-7中的方法可以不返回Optional,而是直接返回一个OptionalInt类型的对象。在第5章中,我们讨论过使用基础类型Stream的场景,尤其是如果Stream对象包含了大量元素,出于性能的考量,使用基础类型是不错的选择,但对Optional对象而言,这个理由就不成立了,因为Optional对象最多只包含一个值。

不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持mapflatMap以及filter方法,而这些是Optional类最有用的方法(正如在11.2节所看到的那样)。此外,与Stream一样,Optional对象无法由基础类型的Optional组合构成,所以,举例而言,如果代码清单11-7中返回的是OptionalInt类型的对象,你就不能将其作为方法引用传递给另一个Optional对象的flatMap方法。

11.4.4 把所有内容整合起来

为了展示之前介绍过的Optional类的各种方法整合在一起的威力,假设你需要向你的程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:

  1. Properties props = new Properties();
  2. props.setProperty("a", "5");
  3. props.setProperty("b", "true");
  4. props.setProperty("c", "-3");

现在,假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。由于一段时间必须是正数,你想要该方法符合下面的签名:

  1. public int readDuration(Properties props, String name)

即,如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返回0。为了明确这些需求,你可以采用JUnit的断言,将它们形式化:

  1. assertEquals(5, readDuration(param, "a"));
  2. assertEquals(0, readDuration(param, "b"));
  3. assertEquals(0, readDuration(param, "c"));
  4. assertEquals(0, readDuration(param, "d"));

这些断言反映了初始的需求:如果属性是areadDuration方法就返回5,因为该属性对应的字符串能映射到一个正数;对于属性b,方法的返回值是0,因为它对应的值不是一个数字;对于c,方法的返回值是0,因为虽然它对应的值是个数字,但它是个负数;对于d,方法的返回值是0,因为并不存在该名称对应的属性。让我们以命令式编程的方式实现满足这些需求的方法,代码清单如下所示。

代码清单 11-8 以命令式编程的方式从属性中读取duration

  1. public int readDuration(Properties props, String name) {
  2. String value = props.getProperty(name);
  3. if (value != null) { ←---- 确保名称对应的属性存在
  4. try {
  5. int i = Integer.parseInt(value); ←---- String属性转换为数字类型
  6. if (i > 0) { ←---- 检查返回的数字是否为正数
  7. return i;
  8. }
  9. } catch (NumberFormatException nfe) { }
  10. }
  11. return 0; ←---- 如果前述的条件都不满足,返回0
  12. }

你可能已经预见,最终的实现既复杂又不具备可读性,呈现为多个由if语句及try/catch块构成的嵌套条件。花几分钟时间思考一下测验11.3,想想怎样使用本章内容实现同样的效果。

测验11.3:使用Optional从属性中读取duration

请尝试使用Optional类提供的特性及代码清单11-7中提供的工具方法,通过一条精炼的语句重构代码清单11-8中的方法。

答案:如果需要访问的属性值不存在,Properties.getProperty(String)方法的返回值就是一个null,使用ofNullable工厂方法可以方便地将该值转换为Optional对象。接着,你可以向它的flatMap方法传递代码清单11-7中实现的OptionalUtility.stringToInt方法的引用,将Optional转换为Optional。最后,你非常轻易地就可以过滤掉负数。这种方式下,如果任何一个操作返回一个空的Optional对象,该方法都会返回orElse方法设置的默认值0;否则就返回封装在Optional对象中的正整数。下面就是这段简化的实现:

  1. public int readDuration(Properties props, String name) {
  2. return Optional.ofNullable(props.getProperty(name))
  3. .flatMap(OptionalUtility::stringToInt)
  4. .filter(i -> i > 0)
  5. .orElse(0);
  6. }

注意到使用OptionalStream时的那些通用模式了吗?它们都是对数据库查询过程的反思,查询时,多种操作会被串接在一起执行。