Optional 使用指南:彻底告别 NPE

前言

到目前为止,臭名昭著的 NullPointerException 是导致 Java 应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了 Optional 类,Guava 通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到 Google Guava 的启发,Optional 类已经成为 Java 类库的一部分。

Optional 类是一个包含了NULL值或者非NULL值的对象容器,它常用作明确表明没有结果(其实明确表明存在结果也可以用Optional表示)的方法返回类型,这样可以避免 NullPointerException 带来的可能的异常。Optional 被定义为一个简单的容器,其值可能是null或者不是null。在 Java8 之前一般某个函数应该返回非空对象,但偶尔还是有可能返回了 null,而在 Java8 中,不推荐你返回null而是返回 Optional。简而言之,Optional 类只是对类进行简单封装,没啥特殊的。本文会逐个探讨 Optional 类包含的方法,并通过一两个示例展示如何使用。

一、Optional 概述

1.1 什么是 Optional?

在我们的开发中,NullPointerException 可谓是随时随处可见,为了避免空指针异常,我们常常需要进行一些防御式的检查,所以在代码中常常可见if(obj != null) 这样的判断。实际项目中会处理大量为空的值,代码会有很多的条件判断,难以阅读与维护。我们从一个简单的用例开始,在 Java 8 之前,任何访问对象方法或属性的调用都可能导致 NullPointerException。如果我们需要确保不触发异常,就得在访问每一个值之前对其进行明确地检查:

java 复制代码
if(user!=null){
  System.out.println(user.getFullName());
} else {
  User defaultUser = new User("Stark", "Tony Stark");
  System.out.println(defaultUser.getFullName());
}

幸好在 JDK1.8 中,Java 为我们提供了一个 Optional 类。Opitonal 是 Java8 引入的一个新类,目的是为了解决空指针异常问题,强制在可能为空的情况下进行显式的处理,以避免空指针异常。本质上,这是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。Optional 类提供了一系列方法来方便地操作内部的值。常用的方法有get、orElse、orElseGet、orElseThrow等。Optional 的设计也考虑了函数式编程的原则,可以与 Lambda 表达式和 Stream API 等特性结合使用,可以进行链式调用替代命令式编程的方式通过编写if条件语句检查null值。Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现,但是 Optional 的意义显然不止于此。

1.2 为什么需要 Optional?

  • 避免 NullPointerException:Optional 类通过提供一种包装机制来解决可能为空的值的问题。它可以将一个值包装在Optional对象中,并通过一些方法提供了安全地访问、操作和处理这个值的方式。使用 Optional 明确表示某个值可能为空,从而迫使开发者在处理值之前考虑为空的情况,避免了直接调用 null 值导致的异常。
  • 提升代码可读性:Optional 提供了一种语义化更好的方式来表示"值可能为空",通过调用 Optional 对象的方法,可以安全地访问包含的值。相比直接返回 null,返回一个 Optional 能清楚地告诉调用方这个值是可选的。Optional 类提供了一些方法来判断Optional对象是否包含非空值,通过调用Optional对象的方法,可以处理值不存在时的情况。
  • 提高代码健壮性:通过 Optional 的 API,如 orElse()、ifPresent()、map() 等,可以以更优雅的方式处理值的存在与否,减少 if-else 的复杂逻辑。通过使用 Optional,可以避免大量的空指针检查和异常处理代码,使得程序更加简洁和安全。

二、Optional 的使用

2.1 构造方法

Optional 是一个容器,容器里面包着一个值,这个值是泛型。因为构造方法私有化,所以不能通过 new 新建 Optional 对象,只能通过静态工厂方法构造对象。

java 复制代码
private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

2.2 创建Optional实例

Optional 类的设计是基于函数式编程的思想,其实现了 java.util.function 包中的 Supplier、Consumer、Predicate、Function 等接口,这使得它可以和 lambda 表达式或者方法引用一起使用,形成更简洁和优雅的代码。Optional 类被 final 修饰,因此它是一个不可变的类,它有几个静态方法用于创建 Optional 对象,如下表所示:

方法 说明
Optional.empty() 创建一个空的 Optional 对象
Optional.of(T value) 创建一个值不为 null 的Optional 对象,参数值不为空,否则会抛出 NPE 异常
Optional.ofNullable(T value) 构造一个可能为空,也可能不为空的 optional 对象

Optional 类似容器,可以包含各种类型的值,也可以为null。若创建的对象为null,则可以采用 empty() 方法,使用 Optional.empty() 方法来创建一个空的Optional实例。例如:

java 复制代码
@Test
public void emptyTest() {
    // 创建一个空的 Optional 对象
    Optional<String> empty = Optional.empty();

    try {
        System.out.println(empty.get());
    } catch (NoSuchElementException exp) {
        System.out.println("抛出 NoSuchElementException 异常");
    }
}

毫不奇怪,尝试访问空 Optional 的值会导致 NoSuchElementException,鉴于此,我们可以使用 of() 和 ofNullable() 方法创建包含值的 Optional。如果明确一个对象实例不为 NULL 的时候,应该使用 Optional.of(),例如:

java 复制代码
@Test
public void ofTest() {
	// 创建一个非空的 Optional 对象
	Optional<String> hello = Optional.of("独泪了无痕");

	try {
		Optional.of(null);
  } catch (NullPointerException exp) {
    System.out.println("抛出 NullPointerException 异常");
  }
}

如果用 of 方法创建 Optional 对象时,所传入的值为 null,则抛出异常。因此,应该明确对象不为 null 的时候使用 of()。如果无法明确一个对象实例是否为 NULL 的时候,就应该使用 ofNullable() 方法。ofNullable() 方法接受一个对象作为参数,如果该对象为 null,它会返回一个空的 Optional 对象。否则,它会将对象包装在 Optional 容器中。

java 复制代码
@Test
public void ofNullableTest() {
    // 创建一个可能为空的 Optional 对象,不管所传入的值是否为null,创建的时候都不会报错
    Optional.ofNullable(null);
    Optional<String> name = Optional.ofNullable("Hello");
}

三、Optional 对象的使用

Optional 对象提供了一些方法,让我们可以更方便地处理可能为空的值,而不需要显式地进行空值检查或者使用 null,以下是一些常用的方法。

3.1 判断值

从 Optional 实例中取回实际值对象的方法之一是使用 get() 方法,不过,从上面示例中就可以看到,这个方法会在值为 null 的时候抛出异常。若是要避免异常,可以选择首先验证是否有值,如下所示:

方法 返回值 作用
isEmpty() boolean 当前对象为 null 返回 true,否则返回 false。
isPresent() boolean 用于判断非空,相当于 != null。如果值存在,返回 true,否则返回 false。
ifPresent(Consumer action) void 如果创建的 Optional 中的值存在,则执行该方法的调用,否则什么也不做
ifPresentOrElse(Consumer action, Runnable emptyAction) void isPresent() 的加强版,值存在时执行第一个操作,否则执行另一个操作。
java 复制代码
/**
 * isPresent方法。判断包装对象是否为空
 */
System.out.println(Optional.of("独泪了无痕").isPresent());
System.out.println(Optional.empty().isPresent());

/**
 * ifPresent方法。不为空时进行操作,内部自动判断null处理
 */
Optional.of("独泪了无痕").ifPresent(System.out::println);
Optional.empty().ifPresent(System.out::println);

3.2 获取值

在我们的开发中,NullPointerException 可谓是随时随处可见,为了避免空指针异常,我们常常需要进行一些防御式的检查,所以在代码中常常可见if(obj != null) 这样的判断。幸好 Java Optional 类能让我们省掉繁琐的非空的判断,下面先说一下Optional中为我们提供的方法。

方法 返回值 作用
T get T 获取 Optional 中的值。如果值为空,则抛出 NoSuchElementException。
T orElse(T other) T 如果有值则将其返回,否则返回指定的other对象。
T orElseGet(Supplier supplier) T 如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象。
T orElseThrow(Supplier exceptionSupplier) void 如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常。

从 Optional 实例中取回实际值对象的方法之一是使用 get() 方法,如果 Optional 对象不包含值,get() 方法将抛出 NoSuchElementException 异常。要避免异常,就可以使用 orElse() 方法提供一个默认值,以便在 Optional 对象不包含值时返回。如果创建的 Optional 中有值存在,则返回此值,否则返回传递给它的参数值。

java 复制代码
@Test
public void orElseTest() {
    System.out.println(Optional.empty().orElse("默认值"));
    System.out.println(Optional.of("Optional对象中的值").orElse("默认值"));
}

在上面的代码中,由于 Optional 实例中不包含值,所以返回了一个默认值。orElseGet 与 orElse 有所不同,这个方法会创建的Optional中有值存在,有值的时候返回值,如果没有值,它会执行作为参数传入的 Supplier(供应者) 函数式接口,并将返回其执行结果。也就是说,其返回的结果是由我们所创建的。

java 复制代码
@Test
public void orElseGetTest() {
    System.out.println(Optional.empty().orElseGet(() -> "默认值"));
    System.out.println(Optional.of("orElseGet").orElseGet(() -> "默认值"));
}

下面我们来看一个示例,orElse、orElseGet二者的区别。

java 复制代码
@Test
public void orElseAndOrElseGetTest() {
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class User {
        private String name;

        public User createUser(String str) {
            System.out.println(str + " 创建的对象--");
            return new User(str);
        }
    }

    User user = new User("独泪了无痕");

    System.out.println(Optional.of(user).orElse(user.createUser("orElse")));
    System.out.println(Optional.of(user).orElseGet(() -> user.createUser("orElseGet")));
}

可以看到,这个示例中,两个 Optional 对象都包含非空值,两个方法都会返回对应的非空值。不过,orElse() 方法仍然创建了 User 对象。与之相反,orElseGet() 方法不创建 User 对象。在执行较密集的调用时,比如调用 Web 服务或数据查询,这个差异会对性能产生重大影响。

除了 orElse() 和 orElseGet() 方法,Optional 还定义了 orElseThrow()。如果创建的 Optional 中有值存在,则返回此值,否则抛出一个由指定的 Supplier 接口生成的异常,而不是返回备选的值。

java 复制代码
@Test
public void orElseThrowTest() {
    @Data
    @NoArgsConstructor
    class User {
        private String name;
    }
    User user = null;
    Optional.ofNullable(user).orElseThrow(IllegalArgumentException::new);
}

这里,如果 user 值为 null,会抛出 IllegalArgumentException。 这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出 NullPointerException

3.3 过滤值

方法 简要说明
Optional filter(Predicate predicate) 通过传入限定条件对 Optional 实例的值进行过滤。 如果有值并且满足条件,返回包含该值的 Optional,否则返回空的 Optional。

filter() 接受一个 Predicate 参数,用于对 Optional 对象中的值进行过滤。如果创建的 Optional 中的值满足 filter 中的条件,则返回包含该值的 Optional 对象,否则返回一个空的 Optional 对象。

java 复制代码
@Test
public void filterTest() {
    System.out.println(Optional.of("zhangsan").filter(e -> e.length() > 5).orElse("王五"));
    Optional<String> emptyOptional = Optional.empty();
    System.out.println(emptyOptional.filter(e -> e.length() > 5).orElse("lisi"));
}

3.4 转换值

Optional 类型还提供了很多方便的方法来对 Optional 对象进行链式调用,有很多种方法可以转换 Optional 的值,我们从 map()、flatMap() 方法开始,可以帮助我们更方便地对 Optional 实例中的值进行操作。

方法 简要说明
Optional map(Function mapper) 如果有值,则对其执行调用 mapper 函数对 Optional 中的值进行转换,返回新的 Optional 对象。 若 optional 的值为空,则 map 方法什么也不会做,直接返回空的 Optional 对象。
Optional flatMap(Function mapper) 如果值存在,返回基于Optional包含的映射方法的值,否则返回空Optional

map() 方法用于将 Optional 对象中的值通过指定的映射函数转换成另外一种类型,先来看一个使用 map() 的例子:

java 复制代码
@Test
public void mapTest() {
    Optional<String> stringOptional = Optional.of("独泪了无痕");
    System.out.println(stringOptional.map(String::toUpperCase).orElse("失败"));
    stringOptional = Optional.empty();
    System.out.println(stringOptional.map(String::toUpperCase).orElse("失败"));
}

flatmap 方法用于扁平化嵌套的 Optional 结构,以避免引入不必要的嵌套层级,具体为 flatmap 的转换函数返回的必须是另一个 Optional 对象,意味着 flatMap 方法可以用于嵌套的 Optional 情况,可以将两个为嵌套关系的 Optional 对象转换为一个。如果原始的 Optional 对象为空,或转换函数返回的 Optional 对象为空,那么最终得到的也是为空的 Optional 对象。

java 复制代码
@Test
public void flatMapTest() {
    Optional<String> optional = Optional.of("独泪了无痕");
    optional.flatMap(str -> Optional.of(str).map(String::length)).ifPresent(System.out::println);
}

在这个例子中,flatMap() 方法将字符串的长度封装在一个新的 Optional 中,并返回这个长度。简单来说,map、flatMap 二者不同,若只需要对 Optional 对象中的值进行转换,而不需要嵌套的 Optional,那么使用 map 方法更合适。如果要进行一些操作返回另外一个 Optional 对象,flatmap 方法更合适。

四、小结

Optional 是 Java 8 中为解决 null 引发问题的一大进步,它通过提供一套清晰的 API,让开发者显式地处理值的存在与否,避免了空指针异常,提升了代码的健壮性和可读性。使用 Optional,不仅可以简化代码,还能让代码更加语义化,是现代 Java 编程中不可或缺的工具。有一点要注意,Optional 是无法被序列化,所以不要试图将 Optional 作为方法参数进行定义,也不要在类当中声明 Optional 类型的成员变量。Optional 通常只作为方法的返回值,用来规避空指针异常。在使用 Optional 时,应该使用函数式的编程风格。

需要注意的是,在使用Optional类时,最好避免过度使用链式操作,以保持代码的可读性和可维护性。同时,建议在需要处理可能为空的值时,使用Optional来替代传统的空指针检查和处理机制。总的来说,这个简单而强大的类有助于创建简单、可读性更强、比对应程序错误更少的程序。

相关推荐
codingandsleeping2 小时前
浏览器的缓存机制
前端·后端
追逐时光者3 小时前
面试官问:你知道 C# 单例模式有哪几种常用的实现方式?
后端·.net
Asthenia04123 小时前
Numpy:数组生成/modf/sum/输出格式规则
后端
Asthenia04123 小时前
NumPy:数组加法/数组比较/数组重塑/数组切片
后端
Asthenia04123 小时前
Numpy:limspace/arange/数组基本属性分析
后端
Asthenia04123 小时前
Java中线程暂停的分析与JVM和Linux的协作流程
后端
Asthenia04123 小时前
Seata TCC 模式:RootContext与TCC专属的BusinessActionContext与TCC注解详解
后端
自珍JAVA3 小时前
【代码】zip压缩文件密码暴力破解
后端
今夜有雨.4 小时前
HTTP---基础知识
服务器·网络·后端·网络协议·学习·tcp/ip·http
Asthenia04124 小时前
Seata TCC 模式的空回滚与悬挂问题之解决方案-结合时序分析
后端