查看原文
其他

Vavr Option:Java Optional 的另一个选项

ImportNew ImportNew 2019-12-01

(给ImportNew加星标,提高Java技能)

编译:ImportNew/唐尤华

dzone.com/articles/using-java-optional-vs-vavr-option



每当涉及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){
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 并通过示例进行了介绍。


希望你喜欢!确保在评论区中留言。


推荐阅读

(点击标题可跳转阅读)

Java Optional 实例介绍

Java 8 如何正确使用 Optional

使用 Java8 Optional 的正确姿势



看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

好文章,我在看❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存