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,将值包装到额外的实例中会降低性能。
相关文章
Do you understand JavaScript closures?
发布时间:2025/02/21 浏览次数:108 分类:JavaScript
-
The function of a closure can be inferred from its name, suggesting that it is related to the concept of scope. A closure itself is a core concept in JavaScript, and being a core concept, it is naturally also a difficult one.
Do you know about the hidden traps in variables in JavaScript?
发布时间:2025/02/21 浏览次数:178 分类:JavaScript
-
Whether you're just starting to learn JavaScript or have been using it for a long time, I believe you'll encounter some traps related to JavaScript variable scope. The goal is to identify these traps before you fall into them, in order to av
How much do you know about the Prototype Chain?
发布时间:2025/02/21 浏览次数:150 分类:JavaScript
-
The prototype chain can be considered one of the core features of JavaScript, and certainly one of its more challenging aspects. If you've learned other object-oriented programming languages, you may find it somewhat confusing when you start
如何在 JavaScript 中合并两个数组而不出现重复的情况
发布时间:2024/03/23 浏览次数:86 分类:JavaScript
-
本教程介绍了如何在 JavaScript 中合并两个数组,以及如何删除任何重复的数组。