理解、接受和利用 Java 中的 Optional

概述

Java 8 引入该语言的最有趣的特性之一是新的Optional类。这个类旨在解决的主要问题是臭名昭著的NullPointerException  ,每个Java程序员都非常清楚。

本质上,这是一个包含可选值的包装类,这意味着它可以包含一个对象,也可以简单地为空。

Optional 伴随着向 Java 函数式编程迈进的一大步 ,旨在帮助实现该范式,但也绝对不在此范围内。

让我们从一个简单的用例开始。在 Java 8 之前,任何涉及访问对象的方法或属性的操作都可能导致NullPointerException

String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

如果我们想确保在这个简短的示例中不会遇到异常,我们需要在访问每个值之前对其进行显式检查:

if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            if (isocode != null) {
                isocode = isocode.toUpperCase();
            }
        }
    }
}

如您所见,这很容易变得繁琐且难以维护。

为了简化这个过程,让我们看一下如何使用Optional类,从创建和验证实例,到使用它提供的不同方法并将其与返回相同类型的其他方法组合,后者是可选谎言的真正力量。

创建可选实例

重申一下,这种类型的对象可以包含一个值或为空。您可以使用具有相同名称的方法创建一个空的 Optional:

@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() {
    Optional<User> emptyOpt = Optional.empty();
    emptyOpt.get();
}

毫不奇怪,尝试访问emptyOpt变量的值会导致NoSuchElementException

要创建一个可以包含值的Optional对象,您可以使用of()和 ofNullable() 方法。两者之间的区别在于,如果将null值作为参数传递给of()方法,它将抛出NullPointerException :

@Test(expected = NullPointerException.class)
public void whenCreateOfEmptyOptional_thenNullPointerException() {
    Optional<User> opt = Optional.of(user);
}

如您所见,我们并没有完全摆脱NullPointerException。因此,只有在确定对象不为null时才应使用of()

如果对象既可以是null也可以不是null,那么您应该选择ofNullable()方法:

Optional<User> opt = Optional.ofNullable(user);

访问可选对象的值

在Optional实例中检索实际对象的一种方法是使用get()方法:

@Test
public void whenCreateOfNullableOptional_thenOk() {
    String name = "John";
    Optional<String> opt = Optional.ofNullable(name);
    
    assertEquals("John", opt.get());
}

但是,如您之前所见,如果值为null,此方法将引发异常。为避免此异常,您可以选择首先验证值是否存在:

@Test
public void whenCheckIfPresent_thenOk() {
    User user = new User("john@gmail.com", "1234");
    Optional<User> opt = Optional.ofNullable(user);
    assertTrue(opt.isPresent());

    assertEquals(user.getEmail(), opt.get().getEmail());
}

检查值是否存在的另一个选项是ifPresent()方法。除了执行检查之外,此方法还接受一个Consumer参数并在对象不为空时执行 lambda 表达式:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));

在此示例中,仅当用户对象不为空时才执行断言。

接下来,让我们看看可以提供空值替代方案的方法。

返回默认值

Optional类提供了用于返回对象值的API,如果对象为空,则返回默认值。

您可以为此目的使用的第一个方法是orElse(),它以一种非常直接的方式工作:如果存在则返回值,如果不存在则返回接收的参数:

@Test
public void whenEmptyValue_thenReturnDefault() {
    User user = null;
    User user2 = new User("anna@gmail.com", "1234");
    User result = Optional.ofNullable(user).orElse(user2);

    assertEquals(user2.getEmail(), result.getEmail());
}

此处,用户对象为空,因此将user2作为默认值返回。

如果对象的初始值不为 null,则忽略默认值:

@Test
public void whenValueNotNull_thenIgnoreDefault() {
    User user = new User("john@gmail.com","1234");
    User user2 = new User("anna@gmail.com", "1234");
    User result = Optional.ofNullable(user).orElse(user2);

    assertEquals("john@gmail.com", result.getEmail());
}

同一类别中的第二个 API 是orElseGet() - 它的行为方式略有不同。在这种情况下,如果存在,则该方法返回值,如果不存在,则执行它作为参数接收的Supplier功能接口,并返回该执行的结果:

User result = Optional.ofNullable(user).orElseGet( () -> user2);

orElse()orElseGet()的区别

乍一看,这两种方法似乎具有相同的效果。但是,情况并非如此。让我们创建一些示例来突出两者之间的相似性和行为差异。

首先,让我们看看当对象为空时它们的行为:

@Test
public void givenEmptyValue_whenCompare_thenOk() {
    User user = null
    logger.debug("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.debug("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

private User createNewUser() {
    logger.debug("Creating New User");
    return new User("extra@gmail.com", "1234");
}

在上面的代码中,这两个方法都调用了createNewUser()方法,该方法记录了一条消息并返回一个User对象。

这段代码的输出是:

Using orElse
Creating New User
Using orElseGet
Creating New User

因此,当对象为空并返回默认对象时,行为没有区别。

接下来,我们来看一个类似的例子,其中Optional不为空:

@Test
public void givenPresentValue_whenCompare_thenOk() {
    User user = new User("john@gmail.com", "1234");
    logger.info("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.info("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

这次的输出是:

Using orElse
Creating New User
Using orElseGet

在这里,两个Optional对象都包含方法将返回的非空值。但是,orElse()方法仍将创建默认的User对象。相比之下,orElseGet()方法将不再创建User对象。

如果执行的操作涉及更密集的调用,例如 Web 服务调用或数据库查询,这种差异可能会对性能产生重大影响。

返回异常

orElse()orElseGet()方法旁边,Optional 还定义了一个orElseThrow() API - 如果对象为空,它不会返回替代值,而是抛出异常:

@Test(expected = IllegalArgumentException.class)
public void whenThrowException_thenOk() {
    User result = Optional.ofNullable(user)
      .orElseThrow( () -> new IllegalArgumentException());
}

在这里,如果用户值为 null,则会引发IllegalArgumentException

这使我们能够拥有更灵活的语义并决定抛出的异常,而不是总是看到NullPointerException

现在我们已经很好地理解了如何单独利用 Optional,让我们看看可用于将转换和过滤应用于Optional值的其他方法。

转变价值观

可选值可以通过多种方式进行转换;让我们从map()和 flatMap()方法开始。

首先,让我们看一个使用map() API 的示例:

@Test
public void whenMap_thenOk() {
    User user = new User("anna@gmail.com", "1234");
    String email = Optional.ofNullable(user)
      .map(u -> u.getEmail()).orElse("default@gmail.com");
    
    assertEquals(email, user.getEmail());
}

map() 将Function参数应用于值,然后返回包装在Optional中的结果。这使得对响应应用和链接进一步的操作成为可能——比如这里的orElse()

相比之下,flatMap()还接受一个Function参数,该参数应用于Optional值,然后直接返回结果。

要查看实际情况,让我们添加一个向User类返回Optional的方法:

public class User {    
    private String position;

    public Optional<String> getPosition() {
        return Optional.ofNullable(position);
    }
    
    //...
}

由于 getter 方法返回 String 值的Optional  ,因此您可以将其用作flatMap()的参数,其中 this 用于Optional User对象。返回将是未包装的字符串值:

@Test
public void whenFlatMap_thenOk() {
    User user = new User("anna@gmail.com", "1234");
    user.setPosition("Developer");
    String position = Optional.ofNullable(user)
      .flatMap(u -> u.getPosition()).orElse("default");
    
    assertEquals(position, user.getPosition().get());
}

过滤值

除了转换值之外,Optional类还提供了根据条件“过滤”它们的可能性。

filter()方法将Predicate作为参数,如果测试结果为真,则返回原样。否则,如果测试为假,则返回值为空Optional

让我们看一个基于非常基本的电子邮件验证接受或拒绝用户的示例:

@Test
public void whenFilter_thenOk() {
    User user = new User("anna@gmail.com", "1234");
    Optional<User> result = Optional.ofNullable(user)
      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"));
    
    assertTrue(result.isPresent());
}

结果对象将包含一个非空值,因为它通过了过滤器测试。

Optional类的链接方法

为了更强大地使用Optional,您还可以链接其大多数方法的不同组合,因为它们中的大多数返回相同类型的对象。

让我们使用Optional重写介绍中的示例。

首先,让我们重构类,以便 getter 方法返回可选引用:

public class User {
    private Address address;

    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }

    // ...
}
public class Address {
    private Country country;
    
    public Optional<Country> getCountry() {
        return Optional.ofNullable(country);
    }

    // ...
}

上面的结构可以直观地表示为一个嵌套集:

现在您可以删除检查并改用Optional方法:

@Test
public void whenChaining_thenOk() {
    User user = new User("anna@gmail.com", "1234");

    String result = Optional.ofNullable(user)
      .flatMap(u -> u.getAddress())
      .flatMap(a -> a.getCountry())
      .map(c -> c.getIsocode())
      .orElse("default");

    assertEquals(result, "default");
}

上面的代码可以通过使用方法引用进一步减少:

String result = Optional.ofNullable(user)
  .flatMap(User::getAddress)
  .flatMap(Address::getCountry)
  .map(Country::getIsocode)
  .orElse("default");

因此,代码看起来比我们早期的繁琐、条件驱动的版本要干净得多。

Java 9 新增功能

在 Java 8 中引入的特性旁边,Java 9 向 Optional 类添加了另外三个方法:or()ifPresentOrElse()stream()

or()方法与orElse()orElseGet()类似,因为它在对象为空时提供替代行为。在这种情况下,返回值是由Supplier参数生成的另一个Optional对象。

如果对象确实包含值,则不执行 lambda 表达式:

@Test
public void whenEmptyOptional_thenGetValueFromOr() {
    User result = Optional.ofNullable(user)
      .or( () -> Optional.of(new User("default","1234"))).get();
                 
    assertEquals(result.getEmail(), "default");
}

在上面的示例中,如果user变量为 null,则返回一个Optional,其中包含一个带有电子邮件“default”的User对象。

ifPresentOrElse ()方法接受两个参数:ConsumerRunnable。如果对象包含一个值,则执行Consumer动作;否则,执行Runnable动作。

如果您想使用存在的值执行操作,或者只是跟踪是否定义了值,则此方法很有用:

Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
  () -> logger.info("User not found"));

最后,新的 stream()方法允许您通过将实例转换为Stream对象来从广泛的Stream API中受益。如果不存在任何值,这将是一个空Stream ,或者一个包含单个值的 Stream - 如果Optional包含值。

让我们看一个将Optional处理为Stream的示例:

@Test
public void whenGetStream_thenOk() {
    User user = new User("john@gmail.com", "1234");
    List<String> emails = Optional.ofNullable(user)
      .stream()
      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
      .map( u -> u.getEmail())
      .collect(Collectors.toList());
   
    assertTrue(emails.size() == 1);
    assertEquals(emails.get(0), user.getEmail());
}

这里使用Stream可以应用Stream接口方法filter()map()collect()来获取List

Optional应该如何 使用

使用Optional时需要考虑一些事项,以确定何时以及如何使用它。

重要的一点是Optional不是Serializable。因此,它不打算用作类中的字段。

如果您确实需要序列化包含Optional的对象, Jackson库支持将Optional视为普通对象。这意味着Jackson将空对象视为null,将具有值的对象视为包含该值的字段。这个功能可以在jackson-modules-java8项目中找到。

另一种使用类型不是很有帮助的情况是作为方法或构造函数的参数。这会导致代码变得不必要地复杂:

User user = new User("john@gmail.com", "1234", Optional.empty());

相反,使用方法重载来处理非强制性参数要容易得多。

Optional的预期用途主要是作为返回类型。获得此类型的实例后,您可以提取该值(如果存在)或提供替代行为(如果不存在)。

Optional类的一个非常有用的用例是将它与返回Optional值的流或其他方法结合起来以构建流畅的 API

让我们看一个使用Stream findFirst()方法的示例,该方法返回一个Optional对象:

@Test
public void whenEmptyStream_thenReturnDefaultOptional() {
    List<User> users = new ArrayList<>();
    User user = users.stream().findFirst().orElse(new User("default", "1234"));
    
    assertEquals(user.getEmail(), "default");
}

结论

Optional是对 Java 语言的有用补充,旨在最大限度地减少代码中NullPointerException的数量,但不能完全删除它们。

它也是对 Java 8 中添加的新功能支持的精心设计且非常自然的补充。

总的来说,这个简单而强大的类有助于创建代码,简而言之,它比其程序对应物更具可读性且不易出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值