java8-Optional使用

在本教程中,我们将展示Java 8中引入的Optional类。 该类的目的是提供一种用于表示可选值而非空引用的类型级别的解决方案。

创建 Optional 对象

创建空对象

1
2
3
4
5
@Test
public void whenCreatesEmptyOptional_thenCorrect() {
Optional<String> empty = Optional.empty();
assertFalse(empty.isPresent());
}

请注意,我们使用了 isPresent() 方法来检查 Optional 对象中是否有一个值。仅当我们使用非空值创建Optional时,该值才存在。我们将在下一节中讨论isPresent方法。

我们还可以使用以下静态方法创建Optional对象

1
2
3
4
5
6
@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
String name = "baeldung";
Optional<String> opt = Optional.of(name);
assertTrue(opt.isPresent());
}

但是,传递给of()方法的参数不能为null。否则,我们将获得NullPointerException:

1
2
3
4
5
@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
String name = null;
Optional.of(name);
}

但是,如果我们期望某些空值,则可以使用 ofNullable() 方法:

1
2
3
4
5
6
@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
String name = "baeldung";
Optional<String> opt = Optional.ofNullable(name);
assertTrue(optionalName.isPresent());
}

这样,如果我们传入一个空引用,它不会抛出异常,而是返回一个空的Optional对象:

1
2
3
4
5
6
@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
String name = null;
Optional<String> opt = Optional.ofNullable(name);
assertFalse(optionalName.isPresent());
}

检查值是否存在 isPresent() 和 isEmpty()

当我们有一个从方法返回或由我们创建的Optional对象时,可以使用isPresent()方法检查其中是否有值:

1
2
3
4
5
6
7
8
@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("Baeldung");
assertTrue(opt.isPresent());

opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
}

如果包装的值不为null,则此方法返回true。

此外,从Java 11开始,我们可以使用isEmpty方法进行相反的操作:

1
2
3
4
5
6
7
8
@Test
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
Optional<String> opt = Optional.of("Baeldung");
assertFalse(opt.isEmpty());

opt = Optional.ofNullable(null);
assertTrue(opt.isEmpty());
}

使用 ifPresent() 的条件操作

如果发现包装值非空,则ifPresent()方法使我们能够对包装值运行一些代码。在可选之前,我们将执行以下操作:

1
2
3
if(name != null) {
System.out.println(name.length());
}

此代码在继续执行一些代码之前检查name变量是否为null。这种方法很冗长,这不是唯一的问题,也容易出错。

如果忘记检查,则会导致空指针异常。

当程序由于输入问题而失败时,通常是由于不良的编程习惯造成的。

使用 Optional 使我们可以显式处理可空值,作为执行良好编程习惯的一种方式。现在让我们看一下如何在Java 8中重构以上代码。

在典型的函数式编程风格中,我们可以对实际存在的对象执行操作:

1
2
3
4
5
@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("baeldung");
opt.ifPresent(name -> System.out.println(name.length()));
}

默认值 orElse()

orElse() 方法用于检索包装在Optional实例内的值。它采用一个参数作为默认值。 orElse() 方法返回包装的值(如果存在),否则返回其参数:

1
2
3
4
5
6
@Test
public void whenOrElseWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("john");
assertEquals("john", name);
}

默认值 orElseGet()

orElseGet() 方法类似于orElse()。但是,如果没有提供Optional值,则不采用返回值,而是采用供应商功能接口,该接口将被调用并返回调用的值:

1
2
3
4
5
6
@Test
public void whenOrElseGetWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
assertEquals("john", name);
}

orElse() 和 orElseGet() 的区别

让我们在测试类中创建一个名为getMyDefault() 的方法,该方法不带任何参数并返回默认值:

1
2
3
4
public String getMyDefault() {
System.out.println("Getting Default Value");
return "Default Value";
}
1
2
3
4
5
6
7
8
9
10
@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
String text = null;

String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Default Value", defaultText);

defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Default Value", defaultText);
}

在上面的示例中,我们在Optional对象中包装了一个空文本,然后尝试使用两种方法中的每一种来获取包装后的值。副作用如下:

1
2
Getting default value...
Getting default value...

在每种情况下都会调用getMyDefault()方法。碰巧的是,当不存在包装的值时,orElse() 和 orElseGet() 的工作方式完全相同。

现在让我们运行另一个测试,其中存在该值,理想情况下,甚至不应创建默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
String text = "Text present";

System.out.println("Using orElseGet:");
String defaultText
= Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Text present", defaultText);

System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Text present", defaultText);
}

在上面的示例中,我们不再包装空值,其余代码保持不变。现在,让我们看一下运行此代码的副作用:

1
2
3
Using orElseGet:
Using orElse:
Getting default value...

请注意,使用orElseGet() 检索包装的值时,由于存在包含的值,因此甚至不会调用getMyDefault() 方法。

但是,使用orElse() 时,无论是否存在包装的值,都会创建默认对象。因此,在这种情况下,我们仅创建了一个从未使用过的冗余对象。

在这个简单的示例中,创建默认对象不会花费很多成本,因为JVM知道如何处理此类对象。但是,当诸如getMyDefault() 之类的方法必须进行Web服务调用或查询数据库时,则成本变得非常明显。

异常 orElseThrow()

orElseThrow() 方法紧随 orElse() 和 orElseGet() 之后,并添加了一种新方法来处理缺少的值。当包装值不存在时,它不会返回默认值,而是会引发异常:

1
2
3
4
5
6
@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow(
IllegalArgumentException::new);
}

使用 get() 返回值

检索包装值的最终方法是 get() 方法:

1
2
3
4
5
6
@Test
public void givenOptional_whenGetsValue_thenCorrect() {
Optional<String> opt = Optional.of("baeldung");
String name = opt.get();
assertEquals("baeldung", name);
}

但是,与上述三种方法不同,get()仅在包装的对象不为null时才能返回值,否则,将引发 no such element 异常:

1
2
3
4
5
@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
Optional<String> opt = Optional.ofNullable(null);
String name = opt.get();
}

这是 get() 方法的主要缺陷。理想情况下,Optional应该可以帮助我们避免此类不可预见的异常。因此,此方法违背Optional的目标,并且可能在以后的版本中弃用。

带条件返回 filter()

我们可以使用filter方法对包装的值进行内联测试。它以断言作为参数,并返回Optional对象。如果包装的值通过断言的测试,则按原样返回Optional。 但是,如果断言返回false,则它将返回空的Optional:

1
2
3
4
5
6
7
8
9
@Test
public void whenOptionalFilterWorks_thenCorrect() {
Integer year = 2016;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
assertTrue(is2016);
boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
assertFalse(is2017);
}

通常以这种方式使用filter方法来基于预定义规则拒绝包装的值。我们可以使用它来拒绝错误的电子邮件格式或强度不够的密码。

让我们看另一个有意义的例子。假设我们要购买调制解调器,而我们只关心它的价格。我们从某个站点接收有关调制解调器价格的推送通知,并将其存储在对象中:

1
2
3
4
5
6
7
8
public class Modem {
private Double price;

public Modem(Double price) {
this.price = price;
}
// standard getters and setters
}

然后,我们将这些对象提供给某些代码,其唯一目的是检查调制解调器的价格是否在我们预算的范围内。

现在让我们看一下没用Optional的代码:

1
2
3
4
5
6
7
8
9
10
11
public boolean priceIsInRange1(Modem modem) {
boolean isInRange = false;

if (modem != null && modem.getPrice() != null
&& (modem.getPrice() >= 10
&& modem.getPrice() <= 15)) {

isInRange = true;
}
return isInRange;
}

请注意要实现此目的必须编写多少代码,尤其是在if条件下。如果条件对应用程序至关重要,则唯一的部分是最后的价格范围检查;其余的检查是防御性的:

1
2
3
4
5
6
7
8
@Test
public void whenFiltersWithoutOptional_thenCorrect() {
assertTrue(priceIsInRange1(new Modem(10.0)));
assertFalse(priceIsInRange1(new Modem(9.9)));
assertFalse(priceIsInRange1(new Modem(null)));
assertFalse(priceIsInRange1(new Modem(15.5)));
assertFalse(priceIsInRange1(null));
}

除此之外,很可能会忘记一整天的空检查而不会出现任何编译时错误。

现在让我们看一下带有Optional#filter的变体:

1
2
3
4
5
6
7
public boolean priceIsInRange2(Modem modem2) {
return Optional.ofNullable(modem2)
.map(Modem::getPrice)
.filter(p -> p >= 10)
.filter(p -> p <= 15)
.isPresent();
}

映射调用仅用于将一个值转换为其他值。请记住,此操作不会修改原始值。

在我们的例子中,我们从 Mode l类中获取一个价格对象。在下一节中,我们将详细介绍 map() 方法。

首先,如果将null对象传递给此方法,则不会有任何问题。

其次,我们在其主体内编写的唯一逻辑就是方法名称所描述的,即价格范围检查。可选的照顾其余的:

1
2
3
4
5
6
7
8
@Test
public void whenFiltersWithOptional_thenCorrect() {
assertTrue(priceIsInRange2(new Modem(10.0)));
assertFalse(priceIsInRange2(new Modem(9.9)));
assertFalse(priceIsInRange2(new Modem(null)));
assertFalse(priceIsInRange2(new Modem(15.5)));
assertFalse(priceIsInRange2(null));
}

先前的方法有望检查价格范围,但除了防御其固有的脆弱性外,还必须做更多的事情。因此,我们可以使用filter方法替换不必要的if语句并拒绝不需要的值。

使用 map() 转换值

在上一节中,我们研究了如何基于过滤器拒绝或接受值。我们可以使用类似的语法通过 map() 方法转换Optional值:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void givenOptional_whenMapWorks_thenCorrect() {
List<String> companyNames = Arrays.asList(
"paypal", "oracle", "", "microsoft", "", "apple");
Optional<List<String>> listOptional = Optional.of(companyNames);

int size = listOptional
.map(List::size)
.orElse(0);
assertEquals(6, size);
}

在此示例中,我们将字符串列表包装在Optional对象中,并使用其map方法对包含的列表执行操作。我们执行的操作是检索列表的大小。

map方法返回包装在Optional中的计算结果。然后,我们必须在返回的Optional上调用适当的方法以获取其值。

请注意,filter方法仅对值进行检查并返回布尔值。另一方面,map方法采用现有值,使用该值执行计算,然后返回包装在Optional对象中的计算结果:

1
2
3
4
5
6
7
8
9
10
@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
String name = "baeldung";
Optional<String> nameOptional = Optional.of(name);

int len = nameOptional
.map(String::length)
.orElse(0);
assertEquals(8, len);
}

我们可以将 map 和 filter 链接在一起,以执行更强大的操作。

假设我们要检查用户输入的密码是否正确;我们可以使用 map 转换清除密码,并使用过滤器检查密码的正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);

correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
assertTrue(correctPassword);
}

如我们所见,如果不先清除输入内容,就会将其过滤掉-但是用户可能会认为前导空格和尾随空格都构成了输入。因此,在过滤掉不正确的密码之前,我们先使用 map 将脏密码转换为干净的密码。

使用 flatMap() 转换值

就像 map() 方法一样,我们也有 flatMap() 方法作为转换值的替代方法。区别在于,map 仅在展开值时才转换值,而flatMap会在转换值之前采用已包装的值并对其进行解包。

以前,我们创建了简单的String和Integer对象,用于包装在Optional实例中。但是,通常,我们将从复杂对象的访问者那里接收这些对象。

为了更清楚地了解两者之间的区别,让我们看一下一个Person对象,该对象带有一个人的详细信息,例如姓名,年龄和密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person {
private String name;
private int age;
private String password;

public Optional<String> getName() {
return Optional.ofNullable(name);
}

public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}

public Optional<String> getPassword() {
return Optional.ofNullable(password);
}

// normal constructors and setters
}

我们通常会创建一个这样的对象并将其包装在Optional对象中,就像处理String一样。或者,可以通过另一个方法调用将其返回给我们:

1
2
Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);

现在注意,当包装一个Person对象时,它将包含嵌套的Optional实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);

Optional<Optional<String>> nameOptionalWrapper
= personOptional.map(Person::getName);
Optional<String> nameOptional
= nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("john", name1);

String name = personOptional
.flatMap(Person::getName)
.orElse("");
assertEquals("john", name);
}

在这里,我们试图检索Person对象的name属性以执行断言。

注意在第三条语句中如何使用map() 方法实现此目的,然后注意之后如何使用flatMap()方法来实现此目的。

Person::getName方法的引用类似于上一节中用于清理密码的String::trim调用。

唯一的区别是 getName() 返回的是Optional而不是String,与 trim() 操作一样。这加上map转换将结果包装在Optional对象中的事实导致嵌套的Optional。

因此,在使用 map() 方法时,我们需要在使用转换后的值之前添加一个额外的调用来检索值。这样,可选包装将被删除。使用 flatMap 时,将隐式执行此操作。

Java8 链式 Optional

有时,我们可能需要从多个Optional中获取第一个非空的Optional对象。在这种情况下,使用类似orElseOptional()的方法将非常方便。不幸的是,Java 8不直接支持这种操作。

让我们首先介绍在本节中将要使用的一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Optional<String> getEmpty() {
return Optional.empty();
}

private Optional<String> getHello() {
return Optional.of("hello");
}

private Optional<String> getBye() {
return Optional.of("bye");
}

private Optional<String> createOptional(String input) {
if (input == null || "".equals(input) || "empty".equals(input)) {
return Optional.empty();
}
return Optional.of(input);
}

为了链接多个Optional对象并获得Java 8中的第一个非空对象,我们可以使用Stream API:

1
2
3
4
5
6
7
8
9
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
Optional<String> found = Stream.of(getEmpty(), getHello(), getBye())
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

assertEquals(getHello(), found);
}

这种方法的缺点是,无论Stream中非空Optional出现在何处,我们所有的get方法都始终执行。

如果我们想懒惰地评估传递给Stream.of()的方法,则需要使用方法参考和Supplier接口:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
Optional<String> found =
Stream.<Supplier<Optional<String>>>of(this::getEmpty, this::getHello, this::getBye)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

assertEquals(getHello(), found);
}

如果需要使用带有参数的方法,则必须求助于lambda表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() {
Optional<String> found = Stream.<Supplier<Optional<String>>>of(
() -> createOptional("empty"),
() -> createOptional("hello")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

assertEquals(createOptional("hello"), found);
}

通常,在所有链接的Optionals为空的情况下,我们通常希望返回一个默认值。我们可以通过添加对orElse()或orElseGet()的调用来做到这一点,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() {
String found = Stream.<Supplier<Optional<String>>>of(
() -> createOptional("empty"),
() -> createOptional("empty")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "default");

assertEquals("default", found);
}

jdk9 Optional Api

Java 9 Optional API Additions

Optional的滥用

最后,让我们看一下使用Optionals的诱人但危险的方法:将Optional参数传递给方法。

想象我们有一个人员列表,我们想要一种方法在该列表中搜索具有给定名称的人。另外,如果指定了年龄,我们希望该方法匹配至少具有一定年龄的条目。由于此参数是可选的,因此我们提供了以下方法:

1
2
3
4
5
6
7
public List<Person> search(List<Person> people, String name, Optional<Integer> age) {
// Null checks for people and name
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age.orElse(0))
.collect(Collectors.toList());
}

然后,我们发布我们的方法,另一个开发人员尝试使用它:

现在,开发人员执行其代码并获取一个NullPointerException。在这里,我们必须对我们的可选参数进行空检查,这在想要避免这种情况时违背了我们的初衷。

我们可能会做一些更好的处理方法:

1
2
3
4
5
6
7
8
9
public List<Person> search(List<Person> people, String name, Integer age) {
// Null checks for people and name

age = age != null ? age : 0;
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}

在那里,该参数仍然是可选的,但是我们仅需检查一下即可处理它。另一种可能性是创建两个重载方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<Person> search(List<Person> people, String name) {
return doSearch(people, name, 0);
}

public List<Person> search(List<Person> people, String name, int age) {
return doSearch(people, name, age);
}

private List<Person> doSearch(List<Person> people, String name, int age) {
// Null checks for people and name
return people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}

这样,我们提供了一个清晰的API,其中包含两种可以完成不同任务的方法(尽管它们共享实现)。

因此,有一些解决方案可以避免使用Optionals作为方法参数。发行Optional时Java的意图是将其用作返回类型,从而表明方法可以返回空值。实际上,某些代码检查人员甚至不建议使用Optional作为方法参数。

参考资源

Oracle Optional文档

原文 Guide To Java 8 Optional

0%
隐藏