是什么

​ 从 Java 8 引入的一个很有趣的特性是 Optional 类。Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException)—— 每个 Java 程序员都非常了解的异常。

本质上,这是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。

Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现。但是 Optional 的意义显然不止于此。

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

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

在这个小示例中,如果我们需要确保不触发异常,就得在访问每一个值之前对其进行明确地检查:

  1. if (user != null) {
  2. Address address = user.getAddress();
  3. if (address != null) {
  4. Country country = address.getCountry();
  5. if (country != null) {
  6. String isocode = country.getIsocode();
  7. if (isocode != null) {
  8. isocode = isocode.toUpperCase();
  9. }
  10. }
  11. }
  12. }

你看到了,这很容易就变得冗长,难以维护。

为了简化这个过程,我们来看看用 Optional 类是怎么做的。从创建和验证实例,到使用其不同的方法,并与其它返回相同类型的方法相结合,下面是见证 Optional 奇迹的时刻。

怎么用

创建 Optional实例

重申一下,这个类型的对象可能包含值,也可能为空。你可以使用同名方法创建一个空的 Optional。

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

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

你可以使用 of()ofNullable() 方法创建包含值的 Optional。两个方法的不同之处在于如果你把 null 值作为参数传递进去,of()方法会抛出 NullPointerException

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

你看,我们并没有完全摆脱 NullPointerException。因此,你应该明确对象不为 null 的时候使用 of()

如果对象可能是 null 也可能是非 null,你就应该使用 ofNullable() 方法

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

访问 Optional对象的值

Optional 实例中取回实际值对象的方法之一是使用 get()方法:

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

不过,这个方法会在值为 null 的时候抛出异常。要避免异常,你可以选择首先验证是否有值:

  1. @Test
  2. public void whenCheckIfPresent_thenOk() {
  3. User user = new User();
  4. Optional<User> opt = Optional.ofNullable(user);
  5. assertTrue(opt.isPresent());
  6. assertEquals(user.getEmail(), opt.get().getEmail());
  7. }
  1. //isPresent()两个重载方法
  2. public boolean isPresent() {
  3. return value != null;
  4. }
  5. public void ifPresent(Consumer<? super T> consumer) {
  6. if (value != null)
  7. consumer.accept(value);
  8. }

检查是否有值的另一个选择是 ifPresent()方法。该方法除了执行检查,还接受一个Consumer(消费者) 参数,如果对象不是空的,就对执行传入的 Lambda 表达式:

  1. @Test
  2. public void whenCheckIfPresent_thenOk() {
  3. Admin admin = new Admin("1234@qq.com", "1234");
  4. Optional<Admin> opt = Optional.ofNullable(admin);
  5. opt.ifPresent( u -> System.out.println(u.getEmail()));
  6. }

这个例子中,只有 admin用户不为 null 的时候才会执行lambda表达式中的代码

接下来,我们来看看提供空值的方法。

返回默认值

Optional 类提供了 API 用以返回对象值,或者在对象为空的时候返回默认值。

这里你可以使用的第一个方法是 orElse(),它的工作方式非常直接,如果有值则返回该值,否则返回传递给它的参数值

  1. @Test
  2. public void whenEmptyValue_thenReturnDefault() {
  3. User user = null;
  4. User user2 = new User("anna@gmail.com", "1234");
  5. User result = Optional.ofNullable(user).orElse(user2);
  6. assertEquals(user2.getEmail(), result.getEmail());
  7. }

这里 user 对象是空的,所以返回了作为默认值的 user2

如果对象的初始值不是 null,那么默认值会被忽略:

  1. @Test
  2. public void whenValueNotNull_thenIgnoreDefault() {
  3. User user = new User("john@gmail.com","1234");
  4. User user2 = new User("anna@gmail.com", "1234");
  5. User result = Optional.ofNullable(user).orElse(user2);
  6. assertEquals("john@gmail.com", result.getEmail());
  7. }

第二个同类型的 API 是 orElseGet()—— 其行为略有不同。这个方法会在有值的时候返回值,如果没有值,它会执行作为参数传入的 Supplier(供应者) 函数式接口,并将返回其执行结果:

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

orElse()orElseGet()的不同之处

乍一看,这两种方法似乎起着同样的作用。然而事实并非如此。我们创建一些示例来突出二者行为上的异同。

我们先来看看对象为空时他们的行为:

  1. @Test
  2. public void givenEmptyValue_whenCompare_thenOk() {
  3. User user = null
  4. logger.debug("Using orElse");
  5. User result = Optional.ofNullable(user).orElse(createNewUser());
  6. logger.debug("Using orElseGet");
  7. User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
  8. }
  9. private User createNewUser() {
  10. logger.debug("Creating New User");
  11. return new User("extra@gmail.com", "1234");
  12. }

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

代码输出如下:

  1. Using orElse
  2. Creating New User
  3. Using orElseGet
  4. Creating New User

由此可见,当对象为空而返回默认对象时,行为并无差异。

我们接下来看一个类似的示例,但这里 Optional 不为空:

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

这次的输出:

  1. Using orElse
  2. Creating New User
  3. Using orElseGet

这个示例中,两个 Optional 对象都包含非空值,两个方法都会返回对应的非空值。不过,orElse() 方法仍然创建了 User 对象。与之相反,orElseGet() 方法不创建User对象

在执行较密集的调用时,比如调用 Web 服务或数据查询,这个差异会对性能产生重大影响

返回异常

除了 orElse()orElseGet() 方法,Optional 还定义了 orElseThrow() API —— 它会在对象为空的时候抛出异常,而不是返回备选的值:

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

这里,如果 user 值为 null,会抛出 IllegalArgumentException

这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出 NullPointerException

现在我们已经很好地理解了如何使用 Optional,我们来看看其它可以对 Optional 值进行转换和过滤的方法。

转换值

有很多种方法可以转换 Optional 的值。我们从 map()flatMap() 方法开始。

先来看一个使用 map() API 的例子:

  1. @Test
  2. public void whenMap_thenOk() {
  3. User user = new User(new Address("北京"));
  4. String country = Optional.ofNullable(user)
  5. .map(User::getAddress) //这一步返回Optional[Address{country='北京'}]
  6. .map(Address::getCountry) //这一不返回Option[北京]
  7. .orElse("China");
  8. }

map()将要转换的值作为参数,然后将其包装在 Optional中返回。这就使链试调用的操作成为可能

相比较下,flatMap()map()作用类似,当参数为null时,两者都返回一个空的Optional,只不过在入参不是null时有些区别。

map()和flatMap()的区别

map会将传入的Function函数的结果进行封装,先看源码:

  1. public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
  2. Objects.requireNonNull(mapper);
  3. if (!isPresent())
  4. return empty();
  5. else {
  6. //会使用Optional的ofNullable方法包装Function函数返回的值
  7. return Optional.ofNullable(mapper.apply(value));
  8. }
  9. }

flatMap会直接返回Function函数执行的结果,看源码:

  1. public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
  2. Objects.requireNonNull(mapper);
  3. if (!isPresent())
  4. return empty();
  5. else {
  6. return Objects.requireNonNull(mapper.apply(value));//直接返回Function执行的结果
  7. }
  8. }
  9. public static <T> T requireNonNull(T obj) {
  10. if (obj == null)
  11. throw new NullPointerException();
  12. return obj;
  13. }

这样看,好像也看不出来两者太大的区别,不知道什么时候用map,什么时候用flatMap,首先对于下列对象的操作

  1. public class School {
  2. private String name;
  3. private Optional<Tearch> tearch; //属性为Option封装的Tearch对象
  4. public String getName() {
  5. return name;
  6. }
  7. public void setName(String name) {
  8. this.name = name;
  9. }
  10. public Optional<Tearch> getTearch() {
  11. return tearch;
  12. }
  13. public void setTearch(Optional<Tearch> tearch) {
  14. this.tearch = tearch;
  15. }
  16. }
  17. class Tearch{
  18. private String name;
  19. private Optional<Student> student;
  20. public String getName() {
  21. return name;
  22. }
  23. public void setName(String name) {
  24. this.name = name;
  25. }
  26. public Optional<Student> getStudent() {
  27. return student;
  28. }
  29. public void setStudent(Optional<Student> student) {
  30. this.student = student;
  31. }
  32. }
  33. class Student{
  34. private String name;
  35. private int age;
  36. public String getName() {
  37. return name;
  38. }
  39. public void setName(String name) {
  40. this.name = name;
  41. }
  42. public int getAge() {
  43. return age;
  44. }
  45. public void setAge(int age) {
  46. this.age = age;
  47. }
  48. }

这时,如果给你一个School对象,让你得到Student的name属性的值,可以使用下面的方式:

  1. public static String getStudentName(School school){
  2. return Optional.ofNullable(school)
  3. .map(School::getTearch)
  4. .map(Tearch::getStudent)
  5. .map(Student::getName)
  6. .orElse("false");
  7. }

你可能感觉这样写很对啊,没毛病,可惜这段代码连编译都不会通过的,我们思考一下,School::getTearch会返回School实例的tearch属性,而tearch属性是使用Optional包装的Tearch对象,所以使用了map(School::getTearch),会返回Optional<Optional<Tearch>>对象,而不是我们所想的Optional<Tearch>

这时就可以使用flatMap来解决这个问题,刚才已经说了,flatMap不会使用Optional包装Function执行的返回结果,所以我们可以使用flatMap来解决这个问题

  1. public static String getStudentName(School school){
  2. return Optional.ofNullable(school)
  3. .flatMap(School::getTearch)
  4. .flatMap(Tearch::getStudent)
  5. .map(Student::getName).orElse("false");
  6. }

这时map和flatMap的用法就清楚了

  • 如果某对象实例的属性本身就为Optional包装过的类型,那么就要使用flatMap方法,就像School::getTearch返回的就是Optional类型的,所以不能再使用Optional进行包装,这时就要选用flatMap方法
  • 对于返回值是其他类型,需要Optional进行包装,如Student::getName得到是String类型的,就需要使用map方法在其外面包装一层Optional对象。

过滤值

除了转换值之外,Optional 类也提供了按条件“过滤”值的方法。

filter() 接受一个Predicate参数,返回测试结果为 true 的值。如果测试结果为 false,会返回一个空的 Optional

来看一个根据基本的电子邮箱验证来决定接受或拒绝 User用户) 的示例:

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

如果通过过滤器测试,result 对象会非空且包含@字符。

Optional 类的链式调用(重要)

我们使用 Optional 重写文章开始处介绍的示例。

  1. User user = null;
  2. String country = Optional.ofNullable(user)
  3. .map(User::getAddress)
  4. .map(Address::getCountry)
  5. .map(Country::getIsocode)
  6. .orElse("default");
  7. System.out.println(country.toUpperCase()); //DEFAULT

上面的嵌套结构可以用下面的图来表示:

Java 9 增强

我们介绍了 Java 8 的特性,**Java 9 为 Optional 类添加了三个方法:or()、*ifPresentOrElse()、stream()。

or() 方法与 orElse()orElseGet() 类似,它们都在对象为空的时候提供了替代情况。or() 的返回值是由 Supplier 参数产生的另一个 Optional 对象。

如果对象包含值,则 Lambda 表达式不会执行:

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

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

ifPresentOrElse() 方法需要两个参数:一个 Consumer 和一个 Runnable。如果对象包含值,会执行 Consumer 的动作,否则运行 Runnable

候执行某个动作,或者只是跟踪是否定义了某个值,那么这个方法非常有用:

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

最后介绍的是新的 stream() 方法,它通过把实例转换为 *Stream* 对象,让你从广大的 Stream API 中受益。如果没有值,它会得到空的 Stream;有值的情况下,Stream 则会包含单一值。

我们来看一个把 Optional 处理成 Stream 的例子:

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

这里对 Stream 的使用带来了其 filter()、map()collect() 接口,以获取 List

结语

在使用 Optional 的时候需要考虑一些事情,以决定什么时候怎样使用它。

重要的一点是Optional不是Serializable。因此,它不应该用作类的字段。

如果你需要序列化的对象包含 Optional 值,Jackson支持把 Optional 当作普通对象。也就是说,Jackson 会把空对象看作 null,而有值的对象则把其值看作对应域的值。这个功能在 jackson-modules-java8 项目中。

它在另一种情况下也并不怎么有用,就是在将其类型用作方法或构建方法的参数时。这样做会让代码变得复杂,完全没有必要:

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

使用重载方法来处理非要的参数要容易得多。

Optional 主要用作返回类型。在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。

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

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

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

总结

Optional 是 Java 语言的有益补充 —— 它旨在减少代码中的 NullPointerExceptions,虽然还不能完全消除这些异常。

它也是精心设计,自然融入 Java 8 函数式支持的功能。

总的来说,这个简单而强大的类有助于创建简单、可读性更强、比对应程序错误更少的程序。

参考文档

文档一

文档二

Java8新特性之Optional,如何优雅地处理空指针的更多相关文章

  1. 【Java8新特性】Optional类在处理空值判断场景的应用 回避空指针异常 编写健壮的应用程序

    一.序言 空值异常是应用运行时常见的异常,传统方式为了编写健壮的应用,常常使用多层嵌套逻辑判断回避空指针异常.Java8新特性之Optional为此类问题提供了优雅的解决方式. 广大程序员朋友对空值异 ...

  2. 乐字节-Java8新特性之Optional

    上一篇小乐带大家了解了Java新特性之Stream,接下来将会继续述说Java新特性之Optional Optional<T>类(java.util.Optional)是一个容器类,代表一 ...

  3. java8新特性之Optional类

    NullPointException可以说是所有java程序员都遇到过的一个异常,虽然java从设计之初就力图让程序员脱离指针的苦海,但是指针确实是实际存在的,而java设计者也只能是让指针在java ...

  4. 【Java8新特性】- Optional应用

    Java8新特性 - Optional应用 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! ...

  5. Java8新特性之Optional

    空指针异常一直是困扰Java程序员的问题,也是我们必须要考虑的.当业务代码中充满了if else判断null 的时候程序变得不再优雅,在Java8中提供了Optional类为我们解决NullPoint ...

  6. java8新特性六-Optional 类

    Optional 类是一个可以为null的容器对象.如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象. Optional 是个容器:它可以保存类型T的值,或者仅仅保 ...

  7. 【Java8新特性】Optional 类

    概述 Optional 类是一个可以为null的容器对象.如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象. Optional 是个容器:它可以保存类型T的值,或者 ...

  8. 乐字节-Java8新特性之Date API

    上一篇文章,小乐给大家带来了Java8新特性之Optional,接下来本文将会给大家介绍Java8新特性之Date API 前言: Java 8通过发布新的Date-Time API来进一步加强对日期 ...

  9. 乐字节-Java8新特性-接口默认方法之Stream流(下)

    接上一篇:<Java8新特性之stream>,下面继续接着讲Stream 5.流的中间操作 常见的流的中间操作,归为以下三大类:筛选和切片流操作.元素映射操作.元素排序操作: 操作 描述 ...

随机推荐

  1. python pip 安装使用国内镜像源

    国内镜像源 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:http://mirrors.aliyun.com/pypi/simple/ 中国科技大学 ...

  2. regexp 正则表达式

    * 给定字符串 str,检查其是否包含连续重复的字母(a-zA-Z),包含返回 true,否则返回 false input: 'rattler' output: true function conta ...

  3. Pycharm新建模板默认添加作者时间等信息(逼格更高,好像很历害的样子)

    在pycharm使用过程中,关于代码编写者的一些个人信息快捷填写,使用模板的方式比较方便. 方法如下: 1.打开pycharm,选择File-Settings 2.选择Editor--Color&am ...

  4. 【vue】获取异步加载后的数据

    异步请求的数据,对它做一些处理,需要怎么做呢?? axios 异步请求数据,得到返回的数据, 赋值给变量 info .如果要对 info 的数据做一些处理后再赋值给 hobby ,直接在 axios ...

  5. GKCTF 2021 Reverse Writeup

    前言 GKCTF 2021所以题目均以开源,下面所说的一切思路可以自行通过源码对比IDA进行验证. Github项目地址:https://github.com/w4nd3r-0/GKCTF2021 出 ...

  6. springcloud组件之注册中心eureka学习

    eureka的高可用 微服务架构中最核心的部分是服务治理,服务治理最基础的组件是注册中心.随着微服务架构的发展,出现了很多微服务架构的解决方案,其中包括我们熟知的Dubbo和Spring Cloud. ...

  7. VulnHub 实战靶场Breach-1.0

    相比于CTF题目,Vulnhub的靶场更贴近于实际一些,而且更加综合考察了知识.在这里记录以下打这个靶场的过程和心得. 测试环境 Kali linux IP:192.168.110.128 Breac ...

  8. 从commons-beanutils反序列化到shiro无依赖的漏洞利用

    目录 0 前言 1 环境 2 commons-beanutils反序列化链 2.1 TemplatesImple调用链 2.2 PriorityQueue调用链 2.3 BeanComparator ...

  9. ArcPy数据列表遍历

    ArcPy数据列表遍历 批处理脚本的首要任务之一是为可用数据编写目录,以便在处理过程中可以遍历数据. ArcPy 具有多个专为创建此类列表而构建的函数. 函数 说明 ListFields(datase ...

  10. 一个简单的Java应用程序

    目录 一个简单的Java应用程序 首次运行结果 程序示例 运行结果 修改大小写之后的运行结果 程序示例 运行结果 关键字public 关键字class 类名及其命名规则 类名必须以字母开头 不能使用J ...