Java SE 8的可选性,一种务实的方法

44 阅读5分钟

Java 8中的Optional classs是帮助开发者管理数据的有用工具。但关于如何使用它的建议却各不相同。这是我对Java 8中使用Optional 的一个好方法的看法。

请注意,本文假设你知道什么是Optional ,以及它是如何工作的。更多信息请参见我之前的文章和其他教程。另外,请注意,Optional 是一个争论激烈的话题,一些评论家容易对其重要性感到过于兴奋。

在Java 8中使用Optional的实用方法

下面是我发现的在Java 8中使用Optional 的具体方法,非常有用。应该考虑的是,这个方法是在编写新的应用程序,而不是维护现有的应用程序的情况下制定的。 有五个基本点。

  1. 不要声明任何类型为Optional 的实例变量。
  2. 使用null 来表示类的私有范围内的可选数据。
  3. 在访问可选字段的getters中使用Optional
  4. 不要在设置器或构造器中使用Optional
  5. 对于任何其他有可选结果的业务逻辑方法,使用Optional 作为返回类型。

比如说。

  public class Address {
    private final String addressLine;  // never null
    private final String city;         // never null
    private final String postcode;     // optional, thus may be null

    // constructor ensures non-null fields really are non-null
    // optional field can just be stored directly, as null means optional
    public Address(String addressLine, String city, String postcode) {
      this.addressLine = Preconditions.chckNotNull(addressLine);
      this.city = Preconditions.chckNotNull(city);
      this.postcode = postcode;
    }

    // normal getters
    public String getAddressLine() { return addressLine; }
    public String getCity() { return city; }

    // special getter for optional field
    public Optional<String> getPostcode() {
      return Optional.ofNullable(postcode);
    }

    // return optional instead of null for business logic methods that may not find a result
    public static Optional<Address> findAddress(String userInput) {
      return ... // find the address, returning Optional.empty() if not found
    }
  }

首先要注意的是,我们的地址API的这个用户被保护不接受null。调用getAddressLine()getCity() 将总是返回一个非空值,因为地址对象在这些字段中不能容纳null。调用getPostcode() 将返回一个Optional<String> 实例,迫使调用者至少要考虑到数据丢失的可能性。最后,findPostcode() 也返回一个Optional 。 这些方法都不能返回null。

在对象内部,开发者仍然被迫考虑null,并使用!= null 检查来管理它。这是合理的,因为null的问题是受限制的。代码将全部作为一个单元来编写和测试(你确实写了测试,不是吗?),所以null不会引起很多问题。

从本质上讲,这种方法所做的是专注于在API边界的返回类型中使用Optional ,而不是在一个类内或输入上。与将其作为一个字段使用相比,optional现在是即时创建的。 这里的关键区别在于Optional 实例的生命周期。

通常情况下,域对象会在内存中停留一段时间,因为应用程序中的处理过程会发生,这使得每个可选实例的寿命相当长(与域对象的寿命有关)。相比之下,从getter返回的Optional 实例可能是非常短暂的。 调用者会调用getter,解释结果,然后继续前进。 如果你对垃圾收集有所了解,你会知道JVM会很好地处理这些短命的对象。 此外,当Optional 实例的寿命很短时,有更多潜在的热点来移除它的费用。虽然很容易声称这是 "不成熟的优化",但作为工程师,我们有责任了解我们工作的系统的限制和能力,并谨慎选择应该强调的点。

虽然这是个小问题,但应该注意的是,该类可以是Serializable ,如果任何字段是Optional ,就不可能做到这一点(因为Optional 没有实现Serializable )。

上面的方法没有对输入使用Optional ,比如设置器或构造器。虽然接受Optional 也可以,但根据我的经验,在设置器或构造器上使用Optional对调用者来说是很烦人的,因为他们通常有实际的对象。 迫使调用者用Optional 包裹参数是我不愿意给用户带来的烦恼。(即方便胜过输入的严格性

在缺点上,这种方法导致对象不是豆子。 获取器的返回类型与字段的类型不匹配,这可能会给一些工具带来问题。 在采用这种方法之前,请检查你使用的任何工具是否能够处理它,例如直接访问字段。

如果在一个应用程序中广泛采用这种方法,null的问题往往会不费吹灰之力地消失。 因为每个域对象都拒绝返回null,所以应用程序往往不会有null的传递。 根据我的经验,采用这种方法往往会导致代码中的null永远不会在类的私有范围之外使用。 而且重要的是,这种情况自然发生,没有痛苦的过渡。 随着时间的推移,你开始减少写防御性代码,因为你更确信没有变量会真正包含null

要使这种方法在基础知识之外发挥作用,关键是要学习Optional 上的各种方法。如果你只是简单地调用Optional.get() ,你就错过了这个类的全部意义。

例如,这里有一些处理XML解析的代码,其中有 "effectiveDate "或 "relativeEffectiveDate "的存在。

 AdjustableDate startDate = tradeEl.getChildOptional("effectiveDate")
   .map(el -> parseKnownDate(el))
   .orElseGet(() -> parseRelativeDate(tradeEl.getChildSingle("relativeEffectiveDate")));

将此分解,tradeEl.getChildOptional("effectiveDate") 返回一个Optional<XmlElement> 。如果找到了该元素,则调用map() 函数来解析该日期。如果没有找到该元素,则调用orElseGet() 函数来解析相对日期。

对于一个使用这种方法的大型企业式代码库Optional ,见OpenGamma Strata,一个用于金融业的现代开源工具包。

也请看Joda-Beans代码生成,它可以生成这种模式(以及更多)。

最后,应该指出的是,未来的某个Java版本,即Java 9之后,可能会支持值类型。在这个未来的世界里,与Optional 相关的成本将消失,更广泛地使用它将是有意义的。我只是认为,现在还不是采用 "到处可选 "方法的时候。

总结

本文概述了在Java 8中使用Optional 的实用方法。如果在一个新的应用程序中持续遵循,null的问题往往就会逐渐消失。

有什么意见吗?