使用 Java8 中的 Optional 类来消除代码中的 null 检查

2023-10-23 17:57:38 浏览数 (1)

本文由 #公众号:一个正经的程序员 原创 作者:散淡样子 GitHub:https://github.com/LouisLiu00

00

前言

Optional 类是 Java8 新增的一个类,Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException)。—— 每个 Java 程序员都非常了解的异常。

本篇文章将详细介绍 Optional 类,以及如何用它消除代码中的 null 检查。

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

01

避免使用 null 检查

作为 Java 开发人员,几乎所有人都遇到过 NullPointerException 异常,大多数人遇到 NullPointerException 异常时都会在异常出现的地方加上 if 代码块来判断值不为空,比如下面的代码:

代码语言:javascript复制
public void bindUserToRole(User user) {
    if (user != null) {
        String roleId = user.getRoleId();
        if (roleId != null) {
            Role role = roleDao.findOne(roleId);
            if (role != null) {
                role.setUserId(user.getUserId());
                roleDao.save(role);
            }
        }
    }
}

这是比较普遍的做法,为了避免出现 NullPointerException 异常,手动对可能为 null 值进行了处理,不过代码看起来非常糟糕,业务逻辑被淹没在 if 逻辑判断中,也许下面的代码看起来可读性稍好一些:

代码语言:javascript复制
public String bindUserToRole(User user) {
    if (user == null) {
        return;
    }

    String roleId = user.getRoleId();
    if (roleId == null) {
        return;
    }

    Role = roleDao.findOne(roleId);
    if (role != null) {
        role.setUserId(user.getUserId());
        roleDao.save(role);
    }
}

上面的代码避免了深层的 if 语句嵌套,但本质上是一样的,方法内有三个不同的返回点,出错后调试也不容易,因为你不知道是哪个值导致了NullPointerException 异常。

基于上面的原因,Java8 中引入了一个新的类 Optional,用以避免使用 null 值引发的种种问题。

02

Optional类

java.util.Optional<T> 类是一个封装了 Optional 值的容器对象,Optional值可以为 null,如果值存在,调用 isPresent() 方法返回 true,调用 get() 方法可以获取值。

创建 Optional 对象

1、Optional 类提供了三个方法用于实例化一个 Optional 对象,它们分别为empty()、of()、ofNullable(),这三个方法都是静态方法,可以直接调用。

empty() 方法用于创建一个没有值的 Optional 对象:

代码语言:javascript复制
Optional<String> emptyOpt = Optional.empty();

empty() 方法创建的对象没有值,如果对 emptyOpt 变量调用 isPresent() 方法会返回 false,调用 get() 方法抛出 NullPointerException 异常。

2、of() 方法使用一个非空的值创建 Optional 对象:

代码语言:javascript复制
String str = "公众号:一个正经的程序员";
Optional<String> notNullOpt = Optional.of(str);

3、ofNullable() 方法接收一个可以为 null 的值:

代码语言:javascript复制
Optional<String> nullableOpt = Optional.ofNullable(str);

如果 str 的值为 null,得到的 nullableOpt 是一个没有值的 Optional 对象。

提取Optional对象中的值

如果我们要获取 User 对象中的 roleId 属性值,常见的方式是直接获取:

代码语言:javascript复制
String roleId = null;

if (user != null) {
    roleId = user.getRoleId();
}

使用 Optional 中提供的 map() 方法可以以更简单的方式实现:

代码语言:javascript复制
Optional<User> userOpt = Optional.ofNullable(user);
Optional<String> roleIdOpt = userOpt.map(User::getRoleId);

使用orElse()方法获取值

Optional 类还包含其他方法用于获取值,这些方法分别为:

  • orElse():如果有值就返回,否则返回一个给定的值作为默认值;
  • orElseGet():与 orElse() 方法作用类似,区别在于生成默认值的方式不同。该方法接受一个 Supplier<? extends T> 函数式接口参数,用于生成默认值;
  • orElseThrow():与前面介绍的 get() 方法类似,当值为 null 时调用这两个方法都会抛出 NullPointerException 异常,区别在于该方法可以指定抛出的异常类型。

下面来看看这三个方法的具体用法:

代码语言:javascript复制
String str = "公众号:一个正经的程序员";
Optional<String> strOpt = Optional.of(str);

String orElseResult = strOpt.orElse("散淡样子");
String orElseGet = strOpt.orElseGet(() -> "散淡样子");
String orElseThrow = strOpt.orElseThrow( () -> new IllegalArgumentException("请填写作者名称!"));

此外,Optional 类还提供了一个 ifPresent() 方法,该方法接收一个 Consumer<? super T> 函数式接口,一般用于将信息打印到控制台:

代码语言:javascript复制
Optional<String> strOpt = Optional.of("公众号:一个正经的程序员");
strOpt.ifPresent(System.out::println);

使用filter()方法过滤

filter() 方法可用于判断 Optional 对象是否满足给定条件,一般用于条件过滤:

代码语言:javascript复制
Optional<String> optional = Optional.of("https://blog.liuxin.online");
optional = optional.filter(str -> str.contains("blog"));

在上面的代码中,如果 filter() 方法中的 Lambda 表达式成立,filter() 方法会返回当前 Optional 对象值;否则,返回一个值为空的 Optional 对象。

03

如何正确使用 Optional

通过上面的例子可以看出,Optional 类可以优雅地避免 NullPointerException 带来的各种问题。

不过,你是否真正掌握了 Optional 的用法?

假设你试图使用 Optional 来避免可能出现的 NullPointerException 异常,编写了如下代码:

代码语言:javascript复制
Optional<User> userOpt = Optional.ofNullable(user);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    // TODO ...
} else {
    // TODO ...
}

坦白说,上面的代码与我们之前的使用 if 语句判断空值没有任何区别,没有起到 Optional 的真正作用:

代码语言:javascript复制
if (user != null) {
    // TODO ...
} else {
    // TODO ...
}

当我们从之前版本切换到 Java8 的时候,不应该还按照之前的思维方式处理 null 值,Java8 提倡函数式编程,新增的许多 API 都可以用函数式编程表示,Optional 类也是其中之一。

更多关于函数式编程请移步至 #公众号:一个正经的程序员 文章:一篇文章教会你使用 Java8 中的 Lambda 表达式

这里有几条关于 Optional 使用的建议:

  • 尽量避免在程序中直接调用 Optional 对象的 get()isPresent() 方法;
  • 避免使用 Optional 类型声明实体类的属性;

第一条建议中直接调用 get() 方法是很危险的做法,如果 Optional 的值为空,那么毫无疑问会抛出 NullPointerException 异常。而为了调用 get() 方法而使用 isPresent() 方法作为空值检查,这种做法与传统的用 if 语句块做空值检查没有任何区别。

第二条建议避免使用 Optional 作为实体类的属性,它在设计的时候就没有考虑过用来作为类的属性,如果你查看 Optional 的源代码,你会发现它没有实现java.io.Serializable 接口,这在某些情况下是很重要的(比如你的项目中使用了某些序列化框架),使用了 Optional 作为实体类的属性,意味着他们不能被序列化。

正确创建 Optional 对象

上面提到创建 Optional 对象有三个方法,empty() 方法比较简单,没什么特别要说明的。

主要是 of() 和 ofNullable() 方法。当你很确定一个对象不可能为 null 的时候,应该使用 of() 方法,否则,尽可能使用 ofNullable() 方法,比如:

代码语言:javascript复制
public static void method(Role role) {
    // 当Optional的值通过常量获得或者通过关键字new初始化,可以直接使用of()方法
    Optional<String> strOpt = Optional.of("公众号:一个正经的程序员");
    Optional<User> userOpt = Optional.of(new User());

    // 方法参数中role值不确定是否为null,使用ofNullable()方法创建
    Optional<Role> roleOpt = Optional.ofNullable(role);
}

orElse() 方法的使用

代码语言:javascript复制
return str != null ? str : "公众号:一个正经的程序员"

上面的代码表示判断字符串 str 是否为空,不为空就返回,否则,返回一个常量。使用 Optional 类可以表示为:

代码语言:javascript复制
return strOpt.orElse("公众号:一个正经的程序员")

简化 if-else

代码语言:javascript复制
User user = ...
if (user != null) {
    String userName = user.getUserName();
    if (userName != null) {
        return userName.toUpperCase();
    } else {
        return null;
    }
} else {
    return null;
}

上面的代码可以简化成:

代码语言:javascript复制
User user = ...
Optional<User> userOpt = Optional.ofNullable(user);
return userOpt.map(User::getUserName).map(String::toUpperCase).orElse(null);

04

总结

总结一下,新的 Optional 类让我们可以以函数式编程的方式处理 null 值,抛弃了 Java8 之前需要嵌套大量 if-else 代码块,使代码可读性有了很大的提高。

0 人点赞