如何用 Java 让审稿人哭泣(可选)
我认为代码审查是最好的灵感来源之一。我把它看作是一个学习新知识的机会,可以从其他提交 pull/merge 请求的软件开发人员那里获得启发。此外,有时你可能需要更深入地研究某个特定主题,特别是与被审查的代码相关的细节。通常,这个过程会让你对该领域有更深刻的理解。然而,代码审查还有另一个事实:一段时间后,你会遇到一些常见的错误。最近,我审查了一个功能,发现其中存在一个Optional 类型的陷阱。说实话,我在不同的合并请求中多次看到过这个问题,这些请求是由不同的开发人员(从新手到经验丰富的开发人员)提交的。
我知道网上有很多关于 Java Optional 的文章,其中一些非常有用,比如Anghel Leonard ( 《Java 编码问题》的作者)写的这篇文章(尽管我对文中提到的一些情况持保留意见) 。我不想重复那些你只需谷歌一下就能找到的内容。我打算谈谈使用Optional时一些最常见但又最少被提及的陷阱。
什么是可选的?
在深入探讨主题之前,让我们先回顾一些定义,了解一下Optional是什么。作为一名 Java 开发人员,您应该遇到过臭名昭著的NullPointerException,它会在您访问空引用时抛出。只需简单搜索一下,您就能找到成千上万个关于 Java(以及其他语言)中空引用的梗图和笑话。Optional在Java 8 中引入,旨在帮助程序员摆脱空引用带来的所有问题。您可以阅读Oracle 技术资源中的这篇文章,了解更多关于Java 中引入Optional 的原因。
现在我们将查看 Optional 的JavaDoc并检查提供的定义。
一个容器对象,其中可能包含非空值,也可能不包含非空值。
继以上介绍之后,作为 API 说明,这里回答一下大家可能会想到的第一个问题:“什么时候应该使用Optional?”
Optional 类型主要用于方法返回值,用于明确表示“无结果”,且使用 null 值很可能导致错误的情况。类型为 Optional 的变量本身永远不应该为 null;它应该始终指向一个 Optional 实例。
因此,我们可以从JavaDoc描述中提取以下几点:
- Optional 是一个容器对象。
- 它可能不包含非空值。
- 它主要用于在使用null值会带来麻烦时作为返回类型。
- Optional 类型的变量不应该为空。
牢记这些要点,你就能轻松发现大多数与Optional相关的代码异味。现在,让我们来看看四个最常见的Optional陷阱。
传统的异常处理
我在代码审查中经常遇到的一个错误是使用 if/else 语句块来处理Optional变量不包含非空值的情况(如前面Optional
定义中所述)。请看这里:
Optional<User> userOptional = userDao.findByCellNumber(cellNumber);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("...");
} else {
User user = userOptional.get();
//...
}
假设你想通过数据库中特定的单元格编号查找用户。你会从相应的 DAO 类中收到一个可选变量。现在,你想检查是否存在具有你提供的单元格编号的用户。上面的代码有什么问题?它不起作用吗?说实话,它运行得非常流畅。但是,它不够优雅;这就像用扳手拧木螺丝一样。现在,请看下面的代码片段:
User user = userDao.findByCellNumber(cellNumber).orElseThrow(UserNotFoundException::new);
使用orElseThrow,您可以更简洁优雅地完成同样的操作,而无需使用get方法。
您还可以使用Java 10 中引入的orElseThrow的无参版本。
orElse 和 orElseGet 滥用
第二个常见的陷阱可能出现在使用orElse或orElseGet时。这些方法看似功能相同,但实际上存在一个显著的区别,虽然不像前一个方法那样显而易见。让我们来看看它们的签名和对应的 Javadoc:
public T orElse(T other)
//If a value is present, returns the value, otherwise returns other.
//Parameters:
//other - the value to be returned, if no value is present. May be null.
//Returns:
//the value, if present, otherwise other
public T orElseGet(Supplier<? extends T> supplier)
//If a value is present, returns the value, otherwise returns the result produced by the supplying function.
//Parameters:
//supplier - the supplying function that produces a value to be returned
//Returns:
//the value, if present, otherwise the result produced by the supplying function
//Throws:
//NullPointerException - if no value is present and the supplying function is null
关于上述签名,第一个区别显而易见:orElse接受一个泛型对象作为参数;而orElseGet接受一个生成值的提供者函数。那又怎样呢?你说得对,这看起来似乎微不足道。然而,事情并非如此简单。让我们来看下面的示例:
Optional<Integer> number = Optional.of(10);
System.out.println("The number is: " + number.orElseGet(() -> alternativeNumber()));
private static Integer alternativeNumber() {
int number = 747;
System.out.println("The alternative number is: " + number);
return number;
}
上述代码片段的输出是什么?如您所见,number 是一个可选变量,初始值为 10。因此,我们并不期望orElseGet执行任何操作。以下是输出结果:
The number is: 10
很简单,对吧?现在看看这个:
Optional<Integer> number = Optional.of(10);
System.out.println("The number is: " + number.orElse(alternativeNumber()));
private static Integer alternativeNumber() {
int number = 747;
System.out.println("The alternative number is: " + number);
return number;
}
除了第二行使用了`orElse`而不是`orElseGet`之外,其他所有代码都与上一个例子相同。我们暂时不期望 ` orElse`执行任何操作,因为我们的数字已经有了值。到此为止,猜猜输出结果!和上一个例子一样吗?不!输出结果是:
The alternative number is: 747
The number is: 10
orElse的参数始终会被求值,即使Optional变量包含值也是如此。然而,传递给orElseGet 的supplier 方法仅在Optional变量为空时才会被求值。这就是第二个输出不同的原因。请注意,如果您使用orElse并向其传递一个复杂的方法,这可能会严重影响性能。
使用 orElse(null)
我在代码审查中经常遇到的另一个常见错误有点哲学意味,那就是使用`orElse(null)`。这样做有什么问题呢?为了找到答案,我们先来回顾一下 `Optional` 存在的初衷。正如我之前提到的,`Optional` 的存在是为了帮助我们避免空引用导致的诸如`NullPointerException`之类的问题。我们在上一节也讨论过`orElse`方法。现在,把这两部分结合起来。如果`Optional`对象为空且不包含任何值,那么要求它返回`null`真的合理吗?
orElse方法是在Optional对象为空时返回替代值的正确方法。返回null会让人质疑Optional的用法。你可能更倾向于使用空引用而不是Optional;这当然可以,但我并不推荐这样做。然而,将这两种方法混用会产生误导。乍一看,代码似乎是空安全的;但是,你迟早会遇到空引用问题。
在返回类型为 Optional 的方法中返回 null
在描述最后一个也是最奇怪的例子之前,我们先来看看它是如何发生的。让我们回到之前的例子。假设findByCellNumber函数是这样的:
public Optional<User> findByCellNumber(String cellNumber) {
//some code here
if (someCondition) {
return null;
}
//some code here
}
如您所见,此方法在特定情况下会返回null。假设数据库宕机,作者处理了异常并在这种情况下返回null(这是不良做法,请勿在家尝试)。因此,在某些情况下,当您尝试获取可选值时,会遇到NullPointerException。请看这里:
User user = userDao.findByCellNumber(cellNumber).orElseThrow(UserNotFoundException::new);
我们期望该方法返回一个Optional 类型,然后我们使用orElseThrow;然而,它返回的是null。因此,我们的 try 语句导致了异常。如何避免这个问题?关键在于:如前所述,Optional类型绝对不能为null。
快速回顾
- 每次显式调用可选变量的get方法都可能是一种代码异味。
- orElse和orElseGet并不等价。
- Java 引入Optional 类型是为了帮助开发者避免空引用。因此,在代码中使用Optional变量时,任何返回 null或使用orElse(null) 的语句都是一种不好的代码习惯。
牢记这些要点,您在使用Optional时可以避免大多数常见陷阱。
文章来源:https://dev.to/dante0747/how-to-make-your-reviewer-cry-using-java-optional-5go3
