迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 > Java >

Java 中 Optionals 的有效使用

作者:迹忆客 最近更新:2022/09/22 浏览次数:

Null 引用的问题

如果你是 Java 开发人员,那么你必须使用 null 来表示“没有值”。而且,我们可能不会在任何 Java 程序中遇到 NullPointerException (NPE)。如果你使用 Java 8 或更高版本编写过代码,那么你可能已经知道 Java 引入了 java.util.Optional<T> 作为 null 的更好替代方案。在本文中,我将解释什么是 Optional 以及如何有效地使用它。但在走得太远之前,我希望大家考虑一下使用 null 的问题,以及 null 如何创建一个有用的(!)异常 (NPE),让所有现有的 Java 开发人员陷入噩梦。

我想提一下在使用 null 时可能遇到的最常见问题,如下所示:

  1. null 是没有意义的。当一个对象引用空引用时,它表示没有值。但是由于算法/逻辑中的错误,很难判断该值实际上是空的(有意/逻辑上)还是尚未初始化或为空。
  2. 不必要的空检查通过增加嵌套缩进和代码冗长而损害可读性。
  3. 一个慷慨(!)的错误来源。由于 Java 不强制检查空值,因此当您忘记检查对象的可空性时,可能会产生烦人的 NullPointerException。
  4. 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 可以为您的代码带来以下好处:

  1. 声明 Optional<T> 类型的变量表示该类型的变量可能包含缺失值。
  2. 通过封装实际值来强制执行“空值检查”。
  3. 可以以功能方式使用。

我们可以想到 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 的转换函数,如 mapflatMapfilter,以组成一系列函数调用(“管道”),每个步骤返回一个单值,该值可以输入到下一个管道。

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 时要记住的事项

  1. Optional的主要用途仅用作方法返回类型。
  2. 类型为 Optional 的变量本身决不能为空; 它应该始终指向一个 Optional 实例。
  3. 由于序列化问题,应避免在字段类型上使用可选,或者可以在 getter/setter 中使用。
  4. 客户端负责处理空的 Optionals。 (不要直接调用 get())
  5. 不要过度使用 Optionals,将值包装到额外的实例中会降低性能。

转载请发邮件至 1244347461@qq.com 进行申请,经作者同意之后,转载请以链接形式注明出处

本文地址:

相关文章

在 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 浏览次数:132 分类:Java

Java 中最常用的顺序是自然顺序。 本文将展示如何使用 naturalOrder() 函数对数组进行排序。

计算 Java 数组中的重复元素

发布时间:2023/05/01 浏览次数:202 分类:Java

本篇文章介绍Java计算数组中重复元素的方法。计算 Java 数组中的重复元素。我们可以创建一个程序来计算数组中的重复元素。 该数组可以是未排序的,也可以是已排序的。

Java 中 List 和 Arraylist 的区别

发布时间:2023/05/01 浏览次数:90 分类:Java

表示为单个单元的一组单个对象称为集合。 在 Java 中,Collection 是一个具有多个已定义接口和类的框架,用于将一组对象表示为一个单元。 它允许我们操纵

扫一扫阅读全部技术教程

社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

最新推荐

教程更新

热门标签

扫码一下
查看教程更方便