首页 > 技术文章 > Vavr Option:Java Optional 的另一个选项

gendan5 2019-11-06 10:57 原文

每当涉及Java,总会有很多选项。 这篇文章讨论了 Java 基础类 Optional 用法,与 Vavr 中的对应方法进行比较。Java 8最早引入了 Optional,把它定义为“一种容器对象,可以存储 null 或非 null 值”。

通常,在返回值可能为null的地方,会出现NullPointerException。开发人员可以使用 Optional 避免 null 值检查。在这种情况下,Optional 提供了一些方便的功能。但可惜的是,Java 8并没有包含所有功能。Optional中的某些功能需要使用 Java 11。要解决这类问题还可以使用 Vavr Option类。

本文将介绍如何使用 Java Optional类,并与 Vavr Option 进行比较。注意:示例代码要求使用Java 11及更高版本。所有代码在 Vavr0.10.2环境下完成测试。

让我们开始吧。

Java Optional 简介

Optional 并不是什么新概念,像 Haskell、Scala 这样的函数式编程语言已经提供了实现。调用方法后,返回值未知或者不存在(比如 null)的情况下,用 Optional 处理非常好用。下面通过实例进行介绍。

新建 Optional 实例

首先,需要获得 Optional 实例,有以下几种方法可以新建 Optional 实例。不仅如此,还可以创建empty Optional。方法一,通过 value 创建,过程非常简单:

Optional<Integer> four = Optional.of(Integer.valueOf(4));
if (four.isPresent){
System.out.println("Hoorayy! We have a value");
} else {
System.out.println("No value");
}

为Integer 4 新建一个Optional实例。这种方法得到的 Optional 始终包含一个 value 且不为 null,例如上面这个示例。使用 ifPresent() 可以检查value是否存在。可以注意到 four 不是 Integer,而是一个装有整数的容器。如果确认 value 存在,可以用 get() 方法执行拆箱操作。具有讽刺意味的是,调用 get() 前如果不进行检查,可能会抛出 NoSuchElementException。

方法二,得到 Optional 对象的另一种方法是使用 stream。Stream提供的一些方法会返回Optional,可以用来检查结果是否存在,例如:

  • findAny 

  • findFirst 

  • max 

  • min 

  • reduce 

查看下面的代码段:

Optional<Car> car = cars.stream().filter(car->car.getId().equalsIgnoreCase(id)).findFirst();

方法三,使用 Nullable 新建 Optional。可能产生 null:

Optional<Integer> nullable = Optional.ofNullable(client.getRequestData());

最后,可以新建一个 empty Optional:

Optional<Integer> nothing = Optional.empty();

如何使用 Optional

获得 Optional 对象后即可使用。一种典型的场景是在 Spring 仓库中根据 Id 查找记录。可以使用 Optional 实现代码逻辑,避免 null 检查(顺便提一下,Spring 也支持 Vavr Option)。比如,从图书仓库里查找一本书。

Optional<Book> book = repository.findOne("some id");

首先,如果有这本书,可以继续执行对应的业务逻辑。在前面的章节用 if-else实现了功能。当然,还有其他办法:Optional 提供了一个方法,接收 Consumer 对象作为输入:

repository.findOne("some id").ifPresent(book -> System.out.println(book));

还可以直接使用方法引用,看起来更简单:

repository.findOne("some id").ifPresent(System.out::println);

如果仓库中没有该书,可以用ifPresentOrElseGet提供回调函数:

repository.findOne("some id").ifPresentOrElseGet(book->{
// 如果 value 存在
}, ()->{
// 如果 value 不存在
});

如果结果不存在,可以返回另一个value:

Book result = repository.findOne("some id").orElse(defaultBook);

但是,Optional 也有缺点,使用时需要注意。最后一个例子中,“确保”无论如何都能获得一本书,可能在仓库中,也可能来自 orElse。但如果默认的返回值不是常量或者需要支持一些复杂方法该怎么办?首先,Java 无论如何都会执行 findOne,然后调用 orElse方法。默认返回值可以为常量,但正如我之前所说那样,执行过程比较耗时。

另一个示例

下面用一个简单的示例介绍如何实际使用 Optional 和 Option 类。有一个 CarRepository,可以根据提供的 ID(比如车牌号)查找汽车,接下来用这个示例介绍如何使用 Optional 和 Option。

首先,加入下面代码

从 POJO 类 Car 开始。它遵循 immutable 模式,所有字段都标记为 final,只包含 getter 没有 setter。初始化时提供所有数据:

public class Car {
   private final String name;
   private final String id;
   private final String color;
   public Car (String name, String id, String color){
       this.name = name;
       this.id = id;
       this.color = color;
   }
   public String getId(){
       return id;
   }
   public String getColor() {
       return color;
   }
   public String getName() {
       return name;
   }
   @Override
   public String toString() {
       return "Car "+name+" with license id "+id+" and of color "+color;
   }
}

接下来创建 CarRepository类。提供两种方法根据Id查找汽车:一种是老办法,使用 Optional。和之前在 Spring 仓库的做法类似,结果可能为 null。

public class CarRepository {
   private List<Car> cars;
   public CarRepository(){
      getSomeCars();
   }
   Car findCarById(String id){
       for (Car car: cars){function(){   //外汇跟单www.gendan5.com            if (car.getId().equalsIgnoreCase(id)){
               return car;
           }
       }
       return null;
   }
   Optional<Car> findCarByIdWithOptional(String id){
       return cars.stream().filter(car->car.getId().equalsIgnoreCase(id)).findFirst();
   }
   private void getSomeCars(){
       cars = new ArrayList<>();
       cars.add(new Car("tesla", "1A9 4321", "red"));
       cars.add(new Car("volkswagen", "2B1 1292", "blue"));
       cars.add(new Car("skoda", "5C9 9984", "green"));
       cars.add(new Car("audi", "8E4 4321", "silver"));
       cars.add(new Car("mercedes", "3B4 5555", "black"));
       cars.add(new Car("seat", "6U5 3123", "white"));
   }
}

注意:初始化过程会在仓库中添加一些汽车模拟数据,便于演示。为了突出重点,避免问题复杂化,下面的讨论专注于 Optional 和 Option。

使用Java Optional

使用JUnit创建一个新测试:

@Test
void getCarById(){
   Car car = repository.findCarById("1A9 4321");
   Assertions.assertNotNull(car);
   Car nullCar = repository.findCarById("M 432 KT");
   Assertions.assertThrows(NullPointerException.class, ()->{
       if (nullCar == null){
           throw new NullPointerException();
       }
   });
}

上面的代码段采用了之前的老办法。查找捷克牌照1A9 4321对应的汽车,检查该车是否存在。输入俄罗斯车牌找不到对应的汽车,因为仓库中只有捷克车。结果为 null 可能会抛出 NullPointerException。

接下来用Java Optional。第一步,获得 Optional 实例,从存储库中使用指定方法返回 Optional:

@Test
void getCarByIdWithOptional(){
   Optional<Car> tesla = repository.findCarByIdWithOptional("1A9 4321");
   tesla.ifPresent(System.out::println);
}

这时调用findCarByIdWithOptional方法打印车辆信息(如果有的话)。运行程序,得到以下结果:

Car tesla with license id 1A9 4321 and of color red

但是,如果代码中没有特定方法该怎么办?这种情况可以从方法返回可能包含 null 的 Optional,称为nullable。

Optional<Car> nothing = Optional.ofNullable(repository.findCarById("5T1 0965"));
Assertions.assertThrows(NoSuchElementException.class, ()->{
   Car car = nothing.orElseThrow(()->new NoSuchElementException());
});

上面这段代码段中,我们发现了另一种方法。通过 findCarById 创建 Optional,如果未找到汽车可以返回 null。没有找到车牌号5T1 0965汽车时,可以用 orElseThrow 手动抛出 NoSuchElementException。另一种情况,如果请求的数据不在仓库中,可以用orElse返回默认值:

Car audi = repository.findCarByIdWithOptional("8E4 4311")
           .orElse(new Car("audi", "1W3 4212", "yellow"));
  if (audi.getColor().equalsIgnoreCase("silver")){
    System.out.println("We have silver audi in garage!");
  } else {
    System.out.println("Sorry, there is no silver audi, but we called you a taxi");
}

好的,车库里没有找到银色奥迪,只好打车了!

使用 Vavr Option

Vavr OptionOption提供了另一种解决办法。首先,在项目中添加依赖,(使用 Maven)安装 Vavr:

<dependency>
  <groupId>io.vavr</groupId>
  <artifactId>vavr</artifactId>
  <version>0.10.2</version>
</dependency>

简而言之,Vavr 提供了类似的 API 新建 Option 实例。可以从 nullable 新建 Option 实例,像下面这样:

Option<Car> nothing = Option.of(repository.findCarById("T 543 KK"));

也可以用 none 静态方法创建一个empty容器:

Option<Car> nullable = Option.none();

此外,还有一种方法可以用 Java Optional 新建 Option。看下面这段代码:

Option<Car> result = Option.ofOptional(repository.findCarByIdWithOptional("5C9 9984"));

使用 Vavr Option,可以使用与 Optional相同的 API 来完成上述任务。例如,设置默认值:

Option<Car> result = Option.ofOptional(repository.findCarByIdWithOptional("5C9 9984"));
Car skoda = result.getOrElse(new Car("skoda", "5E2 4232", "pink"));
System.out.println(skoda);

或者,请求的数据不存在时可以抛出异常:

Option<Car> nullable = Option.none();
Assertions.assertThrows(NoSuchElementException.class, ()->{
nullable.getOrElseThrow(()->new NoSuchElementException());
});

另外,当数据不可用时,可以执行以下操作:

nullable.onEmpty(()->{
///runnable
});

如何根据数据是否存在来执行相应操作,类似 Optional 中 ifPresent?有几种实现方式。与 Optional 中 isPresent 类似,在 Option 中对应的方法称为 isDefined:

if (result.isDefined()){
// 实现功能
}

然而,使用 Option能摆脱 if-else。是否可以用Optional相同的方式完成?使用 peek 操作:

result.peek(val -> System.out.println(val)).onEmpty(() -> System.out.println("Result is missed"));

此外,Vavr Option还提供了一些其他非常有用的方法,在函数式编程上比Optional类效果更好。因此,建议您花一些时间来探索 Vavr Option javadocs尝试使用这些API。我会持续跟进一些类似 map、narrow、isLazy 和 when 这样有趣的功能。

另外,Option只是 Vavr 开发库的一部分,其中还包含了许多其他关联类。不考虑这些类直接与 Optional 比较是不对的。接下来我会继续编写 Vavr 主题的系列文章,介绍 Vavr 相关技术例如 Try、Collections 和 Streams。敬请关注!

总结

本文中,我们讨论了 Java Optional 类。Optional 并不是什么新概念,像 Haskell、Scala这样的函数式编程语言已经提供了实现。调用方法后,返回值未知或者不存在(比如 null)的情况下,Optional 非常有用。然后,介绍了 Optional API,并设计了一个汽车搜索示例进行说明。最后,介绍了 Optional 的另一种替代方案 Vavr Option 并通过示例进行了介绍。

推荐阅读