Java 中 Optionals 的有效使用
Null 引用的问题
如果你是 Java 开发人员,那么你必须使用 null
来表示“没有值”。而且,我们可能不会在任何 Java 程序中遇到 NullPointerException
(NPE)。如果你使用 Java 8 或更高版本编写过代码,那么你可能已经知道 Java 引入了 java.util.Optional<T>
作为 null 的更好替代方案。在本文中,我将解释什么是 Optional
以及如何有效地使用它。但在走得太远之前,我希望大家考虑一下使用 null 的问题,以及 null 如何创建一个有用的(!
)异常 (NPE),让所有现有的 Java 开发人员陷入噩梦。
我想提一下在使用 null 时可能遇到的最常见问题,如下所示:
- null 是没有意义的。当一个对象引用空引用时,它表示没有值。但是由于算法/逻辑中的错误,很难判断该值实际上是空的(有意/逻辑上)还是尚未初始化或为空。
- 不必要的空检查通过增加嵌套缩进和代码冗长而损害可读性。
- 一个慷慨(!)的错误来源。由于 Java 不强制检查空值,因此当您忘记检查对象的可空性时,可能会产生烦人的 NullPointerException。
- null 不携带有关使用意图的信息 - 类型信息,是否允许缺失值等。
让我们通过一些代码示例来理解问题。比方说,我们从一个假想的大学管理系统中有以下域:
public class Student {
private String name;
private Account account;
}
public class Account {
private double balance;
private Loan loan;
}
public class Loan {
private double amount;
}
// 为简单起见,省略了其他字段、getter 和 setter
现在,如果你想检索学生的贷款信息,那么可以编写一个简单的方法,如下所示:
public Double getLoanAmountOfStudent(Student student){
return student.getAccount().getLoan().getAmount();
}
代码的问题是,如果调用中的任何引用为空,则会通过抛出 NPE
导致出现崩溃。 要解决这个问题,我们可以重写如下方法:
public Double getLoanAmountOfStudent(Student student) {
if (student != null) {
if (student.getAccount() != null) {
Account account = student.getAccount();
if (account.getLoan() != null) {
Loan loan = account.getLoan();
if (loan.getAmount() != null) {
return loan.getAmount();
}
}
}
}
return 0d;
}
现在这种方法变得难以阅读,因为每次我们对对象引用有疑问时,都需要检查可空性。 如果你错过了一次空检查,那么它可能会产生 NulPointerException
异常。 另一个问题是返回类型,通过观察方法的返回值,你不能说学生是否没有贷款或贷款金额已变为 0(可能已经还清了贷款)。
Optional<T>
介绍
Java 8 引入了一个受 Haskell 和 Scala 启发的名为 java.util.Optional
的新类。 该类可能包含一个可选值,否则为空值。 Optional 类的目的不是替换每个空引用,而是帮助我们设计可理解的 API,带来更好的可读性,并且显然有助于避免 NPE。
使用 Optionals 可以为您的代码带来以下好处:
-
声明
Optional<T>
类型的变量表示该类型的变量可能包含缺失值。 - 通过封装实际值来强制执行“空值检查”。
- 可以以功能方式使用。
我们可以想到 Optional 对象可以包含三种类型的值——空、可空和非空。 让我们看看如何使用 java.lang.Optional<T>
的静态工厂方法创建不同类型的 Optional。
Empty
Optional 表示没有价值,可以像下面这样创建
Optional.empty()
Nullable
Optional 表示其中的值允许为空,可以像下面这样创建
Optional.ofNullable(value)
Non-null
Optional 表示其中的值必须存在并且可以像下面这样创建
Optional.of(value)
使用 Optional 的示例
所以,我认为大家已经对 Optional 以及如何创建 Optional 对象有了足够的了解。 让我们使用 Optionals 重新设计上述类。
public class Student {
private String name;
private Account account;
}
public class Account {
private Double balance;
private Optional<Loan> loan;
}
public class Loan {
private Double amount;
}
可以看到我只是在 Account 类中将 Loan 类型改为 Optional<Loan>
。 对于开发人员来说,这清楚地表明一个帐户可能没有针对它的贷款,并且是这样计划的。 其他字段与之前保持相同,并表示组合,这意味着指向这些变量的空引用表示代码中缺少数据或错误。
Optional 类的设计者开发它的目的是仅支持 optional-return
习惯用法。 因此 Optional 没有实现 Serializable 接口,并且可能会破坏需要序列化域/类的应用程序。 这就是为什么使用 Optional 作为字段类型是一种反模式。
Optional 主要用作方法返回类型,其中明确需要表示“无结果”,并且使用 null 可能会导致错误。 类型为 Optional 的变量本身决不能为空; 它应该始终指向一个 Optional 实例。
可以通过将 Optional 添加到 getter 的返回类型来解决该问题。 让我们重构这些类来避免这个问题并获得使用 Optionals 的好处。
public class Student {
private String name;
private Account account;
public Optional<Account> getAccount() {
return Optional.of(account);
}
}
public class Account {
private Double balance;
private Loan loan;
public Optional<Loan> getLoan() {
return Optional.ofNullable(loan);
}
}
public class Loan {
private Double amount;
}
现在,让我们使用 Optional 和更新的域重写 getLoanAmountOfStudent
方法。
public Double getLoanAmountOfStudent(Student student) {
Optional<Student> opStudent = Optional.ofNullable(student);
if (opStudent.isPresent() &&
opStudent.get().getAccount().isPresent()) {
Account account = opStudent.get().getAccount().get();
if (account.getLoan().isPresent()) {
return account.getLoan().get().getAmount();
}
}
return 0d;
}
这在代码质量方面要好得多,至少我们不会得到意想不到的 NPE
。 但是它仍然在可读性方面受到影响,因为我们仍然在不必要地产生嵌套的逻辑条件。 这就是命令式编程风格的问题——易于实现但在许多情况下难以阅读。
提示
:避免使用 isPresent() 和 get() 对,它们并不优雅
Optional Monad
让我们考虑一下使用函数式编程原则编写程序时声明式的实现方式。 我们可以将 Optional 视为 Monad。 monad 是一种包装另一种类型并为基础类型提供某种形式的质量的类型。 Optional Monad 的作用类似于包装可能为 null 值的 monad,如果值在操作之间没有变为 null,则允许执行一些转换,并提供一种提取结果值的方法。 java.lang.Optional
提供类似于 Stream API 的转换函数,如 map
、flatMap
和 filter
,以组成一系列函数调用(“管道”),每个步骤返回一个单值,该值可以输入到下一个管道。
public Double getLoanAmountOfStudent(Student student) {
return Optional.ofNullable(student)
.flatMap(Student::getAccount)
.flatMap(Account::getLoan)
.map(Loan::getAmount)
.orElse(0d);
}
不建议使用 Optional 作为方法参数,因为它会创建额外的包装层。
Optional 流式传输
比方说,我们想找到有贷款的学生人数。 我们可以为此编写如下方法:
public long countStudentHavingLoan(List<Student> students) {
return students.stream()
.map(Student::getAccount)
.map(acc -> acc.flatMap(Account::getLoan))
.filter(Optional::isPresent)
.map(Optional::get)
.count();
}
看,这里我们正在对学生列表进行流式传输,转换为帐户,然后从每个帐户中提取贷款。 问题是学生的每个账户都没有贷款。 所以在 Stream<Optional<Loan>>
的流中,我们可能会得到空的optional。 为了摆脱空 optional ,我们使用过滤器和映射来获取非空optional。 最后过滤和统计有贷款的学生人数。
从 Java 9 开始,在 Optional 类中引入了 stream()
方法,可用于将可选元素的 Stream 转换为当前值元素的 Stream。 在这种情况下,它可能看起来很方便。 请看下面的代码:
public long countStudentHavingLoan(List<Student> students) {
return students.stream()
.map(Student::getAccount)
.map(acc -> acc.flatMap(Account::getLoan))
.flatMap(Optional::stream)
.count();
}
在这里,我们可以看到我们使用了 Optional::stream
,它通过一个操作直接将 Stream<Optional<Loan>>
转换为 Stream<Loan>
并删除空的 Optional。
返回计算值时使用 Optional Lazily
假设,我们正在编写一个通过 id 查找学生的方法。 为此,首先我们尝试在缓存中查找,如果未找到,则查询数据库并检索学生,否则抛出异常。 该方法如下所示:
public Student findStudent(String id) {
return studentCache.getStudent(id)
.orElse(studentService.getStudent(id)
.orElseThrow(() -> new
NotFoundException("Student is not found with id" + id))
);
}
即使在缓存中找到学生,上面的代码也会同时查询缓存和数据库。 orElse()
即使值存在也会被调用。 为避免调用数据库,我们可以使用 orElseGet(Supplier<? extends T> supplier)
,它仅在值为空时进行评估。
public Student findStudentById(String id) {
return studentCache.getStudent(id)
.orElseGet(() ->
studentService.getStudent(id)
.orElseThrow(() -> new
NotFoundException("Student is not found with id" + id))
);
}
从 Java 9 开始,Optional
已通过 or(Supplier<? extends Optional<? extends T>> supplier)
方法得到增强,该方法可以执行操作并返回 Optional 而不是直接值。 因此,上述方法可以进一步重构如下:
public Optional<Student> findStudentById(String id) {
return cacheService.GetStudent(id)
.or(() -> studentServiceGetStudent(id));
}
使用 Optional 时要记住的事项
- Optional的主要用途仅用作方法返回类型。
- 类型为 Optional 的变量本身决不能为空; 它应该始终指向一个 Optional 实例。
- 由于序列化问题,应避免在字段类型上使用可选,或者可以在 getter/setter 中使用。
- 客户端负责处理空的 Optionals。 (不要直接调用 get())
- 不要过度使用 Optionals,将值包装到额外的实例中会降低性能。
相关文章
在 Java 中获取文件大小
发布时间:2023/05/01 浏览次数:139 分类:Java
-
Java 提供了不同的方法来获取文件的字节大小。 本教程演示了在 Java 中获取文件大小的不同方法。使用 Java IO 的文件类获取文件大小 Java IO 包的 File 类提供了以字节为单位获取文件大小的功能。
Java 中的文件分隔符
发布时间:2023/05/01 浏览次数:108 分类:Java
-
本篇文章介绍了 Java 中的文件分隔符。Java 中的文件分隔符 文件分隔符是用来分隔目录的字符; 例如,Unix 使用 /,Windows 使用 \ 作为文件分隔符。
Java 中的文件过滤器
发布时间:2023/05/01 浏览次数:193 分类:Java
-
本篇文章介绍如何在 Java 中使用 FileFilter。FileFilter 用于过滤具有特定扩展名的文件。 Java内置包IO和Apache Commons IO为FileFilter提供了类和接口来进行文件过滤操作。
Java 获取 ISO 8601 格式的当前时间戳
发布时间:2023/05/01 浏览次数:132 分类:Java
-
本篇文章介绍了 ISO 8601 日期格式、其重要性及其在 Java 中的使用。 它还列出了一些优点来强调为什么应该使用 ISO 格式来表示日期。
在 Java 中获取数组的子集
发布时间:2023/05/01 浏览次数:142 分类:Java
-
本篇文章介绍了几种在 Java 中获取数组子集的方法。使用 Arrays.copyOf() 方法获取数组的子集 使用 Arrays.copyOfRange() 方法获取数组的子集
用 Java 填充二维数组
发布时间:2023/05/01 浏览次数:110 分类:Java
-
二维数组是基于表结构的,即行和列,填充二维数组不能通过简单的添加到数组操作来完成。 本篇文章介绍如何在 Java 中填充二维数组。
计算 Java 数组中的重复元素
发布时间:2023/05/01 浏览次数:202 分类:Java
-
本篇文章介绍Java计算数组中重复元素的方法。计算 Java 数组中的重复元素。我们可以创建一个程序来计算数组中的重复元素。 该数组可以是未排序的,也可以是已排序的。
Java 中 List 和 Arraylist 的区别
发布时间:2023/05/01 浏览次数:90 分类:Java
-
表示为单个单元的一组单个对象称为集合。 在 Java 中,Collection 是一个具有多个已定义接口和类的框架,用于将一组对象表示为一个单元。 它允许我们操纵