article/java/Optional正确的使用方法.md
2023-03-05 17:53:40 +08:00

118 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

已剪辑自: https://blog.kaaass.net/archives/764
Optional 是 Java8 提供的为了解决 null 安全问题的一个 API。善用 Optional 可以使我们代码中很多繁琐、丑陋的设计变得十分优雅。这篇文章是建立在你对 Optional 的用法有一定了解的基础上的,如果你还不太了解 Optional可以先去看看相关教程或者查阅 [Java 文档](http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html)。
使用 Optional我们就可以把下面这样的代码进行改写。
```java
public static String getName(User u) {
if (u == null || u.name == null)
return "Unknown";
return u.name;
}
```
不过,千万不要改写成这副样子。
```Java
ublic static String getName(User u) {
Optional<User> user = Optional.ofNullable(u);
if (!user.isPresent())
return "Unknown";
return user.get().name;
}
```
这样改写非但不简洁,而且其操作还是和第一段代码一样。无非就是用 isPresent 方法来替代 u==null。这样的改写并不是 Optional 正确的用法,我们再来改写一次。
```java
public static String getName(User u) {
return Optional.ofNullable(u)
.map(user->user.name)
.orElse("Unknown");
}
```
这样才是正确使用 Optional 的姿势。那么按照这种思路,我们可以安心的进行链式调用,而不是一层层判断了。看一段代码:
```java
public static String getChampionName(Competition comp) throws IllegalArgumentException {
if (comp != null) {
CompResult result = comp.getResult();
if (result != null) {
User champion = result.getChampion();
if (champion != null) {
return champion.getName();
}
}
}
throw new IllegalArgumentException("The value of param comp isn't available.");
}
```
由于种种原因(比如:比赛还没有产生冠军、方法的非正常调用、某个方法的实现里埋藏的大礼包等等),我们并不能开心的一路 comp.getResult().getChampion().getName() 到底。而其他语言比如 kotlin就提供了在语法层面的操作符加持comp?.getResult()?.getChampion()?.getName()。所以讲道理在 Java 里我们怎么办!
让我们看看经过 Optional 加持过后,这些代码会变成什么样子。
```java
public static String getChampionName(Competition comp) throws IllegalArgumentException {
return Optional.ofNullable(comp)
.map(Competition::getResult) // 相当于c -> c.getResult(),下同
.map(CompResult::getChampion)
.map(User::getName)
.orElseThrow(()->new IllegalArgumentException("The value of param comp isn't available."));
}
```
这就很舒服了。Optional 给了我们一个真正优雅的 Java 风格的方法来解决 null 安全问题。虽然没有直接提供一个操作符写起来短,但是代码看起来依然很爽很舒服。更何况 ?. 这样的语法好不好看还见仁见智呢。
还有很多不错的使用姿势,比如字符串为空则不打印可以这么写:
```java
string.ifPresent(System.out::println);
//Optional 的魅力还不止于此Optional 还有一些神奇的用法,比如 Optional 可以用来检验参数的合法性。
public void setName(String name) throws IllegalArgumentException {
this.name = Optional.ofNullable(name)
.filter(User::isNameValid)
.orElseThrow(()->new IllegalArgumentException("Invalid username."));
}
```
这样写参数合法性检测,应该足够优雅了吧。
2019-10-13 补充 Optional 的本质,提出若干应用建议。
不过这还没完,上面的两个例子其实还不能完全反应出 Optional 的设计意图。事实上,我们应该更进一步,减少 Optional.ofNullable 的使用。为什么呢?因为 Optional 是被设计成用来代替 null 以表示不确定性的,换句话说,只要一段代码可能产生 null那它就可以返回 Optional。而我们选择用 Optional 代替 null 的原因,是 Optional 提供了一个把**若干依赖前一步结果**的处理结合在一起的途径。举个例子,在我们调用一个网站的登录接口的时候,大概会有以下的步骤:
1. 发送 HTTP 请求,得到返回。
2. (依赖:接口的返回)解析返回,如将 Json 文本形式的返回结果转化为对象形式。
3. (依赖:解析的结果)判断结果是否成功。
4. (依赖:若成功调用的结果)取得鉴权令牌。
5. (依赖:获得的令牌)进行处理。
其中,第 2-5 步的每一个步骤都依赖于前一个步骤,而前一个步骤传递过来的数据都具不确定性(有可能是 null。所以我们可以把它们接受的数据都设计成 Optional。第 1-4 步每一个步骤的结果也具备不确定性,所以我们也把它们的结果设计成 Optional。最后到了第 5 步,我们终于要对一切的结果进行处理了:如果成功获得令牌就存储,失败就提示用户。所以这一步,我们采用如 orElse 之类的方法来**消除**不确定性。于是我们最后的设计就可以是:
1. 结果 String可能是 null **==** **包装** **==>** Optional<String>
2. Optional<String> **==** **解析** **==>** Optional<Json对象>
3. Optional<Json对象> **== Filter** **判断成功** **==>** Optional<Json对象>
4. Optional<Json对象> **==** **取鉴权令牌** **==>** Optional<AuthToken>
5. 对 Optional<AuthToken> 进行处理,消除 Optional
Optional 就像一个处理不确定性的管道,我们在一头丢进一个可能是 null 的东西(接口返回结果),经过层层处理,最后消除不确定性。**Optional 在过程中保留了不确定性,从而把对 null 的处理移到了若干次操作的最后,以减少出现 NPE 错误的可能。**于是Optional 应用的建议也呼之欲出了:
1. 适用于层级处理(依赖上一步操作)的场合。
2. 产生对象的方法若可能返回 null可以用 Optional 包装。
3. 尽可能延后处理 null 的时机,在过程中使用 Optional 保留不确定性。
4. 尽量避免使用 Optional 作为字段类型。
最后说句题外话,这种依赖上一步的操作也叫 Continuation。而 Optional 的这种接受并组合多个 Continuation 的设计风格就是 Continuation-passing styleCPS
# 参考资料
使用 Java8 Optional 的正确姿势 隔叶黄莺 Unmi Blog (https://unmi.cc/proper-ways-of-using-java8-optional/)