Java8-Optional使用心得

引言

NPE(NullPointerException)是很讨人厌的东西,比如它让你在写代码时总担心你不熟悉的 API 返回的是不是一个可空的类型。

Kotlin 有令人感到兴奋的空安全机制,即引入了 T? 表示类型 T | null,感兴趣可以了解下。

Java 8 引入了 Optional,似乎可以作为 T? 的一种替代方案,但事实好像并非如此?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class Optional<T> {
//Null指针的封装
private static final java.util.Optional<?> EMPTY = new java.util.Optional<>();
//内部包含的值对象
private final T value;
private Optional() ;
//返回EMPTY对象
public static<T> java.util.Optional<T> empty() ;
//构造函数,但是value为null,会报NPE
private Optional(T value);
//静态工厂方法,但是value为null,会报NPE
public static <T> java.util.Optional<T> of(T value);
//静态工厂方法,value可以为null
public static <T> java.util.Optional<T> ofNullable(T value) ;
//获取value,但是value为null,会报NoSuchElementException
public T get() ;
//返回value是否为null
public boolean isPresent();
//如果value不为null,则执行consumer式的函数,为null不做事
public void ifPresent(Consumer<? super T> consumer) ;
//过滤,如果value不为null,则根据条件过滤,为null不做事
public java.util.Optional<T> filter(Predicate<? super T> predicate) ;
//转换,在其外面封装Optional,如果value不为null,则map转换,为null不做事
public<U> java.util.Optional<U> map(Function<? super T, ? extends U> mapper);
//转换,如果value不为null,则map转换,为null不做事
public<U> java.util.Optional<U> flatMap(Function<? super T, java.util.Optional<U>> mapper) ;
//value为null时,默认提供other值
public T orElse(T other);
//value为null时,默认提供other值
public T orElseGet(Supplier<? extends T> other);
//value为null时,默认提供other值
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) ;
}

关于 Optional 的 Usage,网上很多文章了,这里不赘述

他人观点

参与设计 Java 语言的 Oracle 工程师 Brian Goetz 在 Stack Overflow 上说过:

  • Optional 不是一个通用的 Maybe 类型(即没有变成类型系统上类似 T?T! 的设计,尽管很多人希望我们这么做)
  • 我们的目标是提供一个有限的机制,为库方法的返回类型提供一种清晰的表示 No Result 的方式(在某些情况使用 null 极有可能会导致错误)
  • 我认为 Optional 的设计没有错,尽管它不像很多人希望的那样。
  • 但确实应该避免过度使用 Optional(我们担心过度使用的风险)

其他观点:

  • Optional 是伴随着 Java8 中函数式编程出现,在 JDK 代码中,Optional 大都用在 Stream 中

公认事项

一些使用误区:

  • 包装数组或容器中的类型 public List<Optional<A>> get();
  • 作为字段
  • 作为方法参数 void doSth(Optional<A> a);
  • 过度用作 getter 的返回值

一些注意事项:

  • 永远不要用 Optional.get,除非你可以证明它不为空(但是先调用 Optional.isPresent 再调用 Optional.get 明显就是一种过度使用)。可选择用 Optional.orElseOptional.ifPresent
  • Optional 不能序列化

个人之言

以下结合我个人的实际编码经历,来尝试总结一下

不要为了用而用

函数式编程有了 Optional,能更写出更流畅的代码逻辑。但不是说我们平时按命令式语言的逻辑写代码时,一定要转成函数式去写(我年轻时候喜欢这样写,是有种炫技的感觉?)

下面我拿几个真实的例子来循序渐进地说明一下(可以见仁见智地一同分析一下)

  • 第一种:宁愿别用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 例子1
Optional.of(reqDTO)
.map(ContestSubmissionListReqDTO::getProblemCode)
.filter(StringUtils::isNotEmpty).ifPresent(problemCode -> {
try {
reqDTO.setProblemIndex(Integer.parseInt(reqDTO.getProblemCode()));
} catch (Exception e) {
throw new ApiException(ApiExceptionEnum.PARAMETER_ERROR, "problemCode 非法");
}
});
// 不用 Optional
if (StringUtils.isNotEmpty(reqDTO.getProblemCode())) {
try {
reqDTO.setProblemIndex(Integer.parseInt(reqDTO.getProblemCode()));
} catch (Exception e) {
throw new ApiException(ApiExceptionEnum.PARAMETER_ERROR, "problemCode 非法");
}
}

// 例子2
Optional.ofNullable(problemCodeToMetrics.get(p.getProblemCode())).ifPresent(metrics -> {
p.setAcceptNum(metrics.getAcceptNum());
p.setSubmitNum(metrics.getSubmitNum());
});
// 不用 Optional
SubmissionMetricsDTO metrics = problemCodeToMetrics.get(p.getProblemCode());
if (metrics != null) {
p.setAcceptNum(metrics.getAcceptNum());
p.setSubmitNum(metrics.getSubmitNum());
}
  • 第二种:可用可不用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 例子3
Optional.ofNullable(redisPassword)
.filter(StringUtils::isNotEmpty)
.map(RedisPassword::of)
.ifPresent(configuration::setPassword);
// 不用 Optional
if (StringUtils.isNotEmpty(redisPassword)) {
configuration.setPassword(RedisPassword.of(redisPassword));
}

// 例子4
public Integer getMaxMemoryFactor() {
return Optional.ofNullable(maxMemoryFactor).orElse(1);
}
// 不用 Optional
public Integer getMaxMemoryFactor() {
return maxMemoryFactor != null ? maxMemoryFactor : 1;
}

// 例子5
Long userId = Optional.ofNullable(userSessionDTO).map(UserSessionDTO::getUserId).orElse(null);
// 不用 Optional
Long userId = userSessionDTO != null ? userSessionDTO.getUserId() : null;

// 例子6
Optional.ofNullable(reqDTO.getContestId())
.ifPresent(contestId -> query.eq(SubmissionDO::getContestId, contestId));
// 不用 Optional
if (reqDTO.getContestId() != null) {
query.eq(SubmissionDO::getContestId, reqDTO.getContestId());
}

// 例子7
String emailCode = Optional.ofNullable((String) redisUtils.get(redisKey)).orElseGet(() -> CaptchaUtils.getRandomString(6));
// 不用 Optional
String emailCode = (String) redisUtils.get(redisKey);
if (emailCode == null) {
emailCode = CaptchaUtils.getRandomString(6);
}
  • 第三种,用得有点别扭(乍一看挺爽,仔细想想又觉得代码可读性不太好)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 例子8(以前在公司看到的代码,已脱敏)
boolean oneBoolean = true;
Map<String, String> fMap = oneDTO.getFeatureMap();
if (fMap != null && fMap.containsKey("oneKey") && StringUtils.isNumeric(fMap.get("oneKey"))) {
Long options = Long.valueOf(fMap.get("oneKey"));
if (options != null && ((options>>>48) & 1) == 1) {
// 第48位为1,表示"一种特殊的含义"
oneBoolean = false;
}
}
// 假如改造成 Optional(且要求我只能用一条链式调用完成这个功能)
boolean oneBoolean = Optional.ofNullable(fulfillWareDTO.getFeatureMap())
.map(map -> map.get("oneKey"))
.filter(StringUtils::isNumeric)
.map(Long::parseLong)
.map(options -> ((options >>> 48) & 1) != 1) // 注意这里用的是反逻辑,十分不好理解
.orElse(true);
  • 第四种,这样用感觉还行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 例子9(以前在公司看到的代码)
private String getSellerNick(String sellerId){
if (StringUtils.isEmpty(sellerId)){
return null;
}
Result<UserDO> result = userReadService.getUserByUserId(Long.valueOf(sellerId));
if (result != null && result.isSuccess() && result.getData() != null){
return result.getData().getNick();
}
return null;
}
// 假如改造成 Optional
private String getSellerNick(String sellerId) {
return Optional.ofNullable(sellerId)
.filter(StringUtils::isNumeric)
.map(Long::parseLong)
.map(sellerIdLong -> userReadService.getUserByUserId(sellerIdLong))
.map(Result::getData)
.map(UserDO::getNick)
.orElse(null);
}

// 例子10,官方的例子
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}
// 用 Optional(这种确实是值得使用的,可读性好)
String name = Optional.ofNullable(computer)
.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
// 如果有 Kotlin 的空安全则更爽
String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

性能?

如果你看过 Java8 写的 Lambda 代码的字节码就会发现,匿名函数、函数式方法,都变成了一个个 synthetic 关键字的方法,即是 Java 编译器在编译器生成的方法。

所以其实看起来调用很爽,但其实发生了更多的方法调用。上面例子中的 “可用可不用” 种类中,是基于编码角度及其可读性来评判的,但如果再加上一个考虑点 “性能”,则 “可用可不用” 种类的代码应该不要用 Optional,此时显然调用两次 getter 的代价比调 Lambda 小得多!

null 自身语义?

有时候在设计技术方案的时候会发现,如果变量的每种可能值都代表一个语义,刚好设计失误,就会有语义不够用的情况,此时 null 可能表示 “无,没有,忽略,不作用” 也可能表示 “空值”。

下面举个例子:在使用 MyBatis-Plus 这个库的时候,有一种更新数据库中一行数据的接口方法

1
2
3
public interface BaseMapper<T> extends Mapper<T> {
int updateById(@Param(Constants.ENTITY) T entity);
}
  • 调用 BaseMapper::updateById 方法,将一个 entity 传进去,它将 entity 中 ID 属性作为 SQL 中 Where 条件,忽视所有值为 null 的字段,将非 null 字段的值作为更新后的新值,实现更新一行的操作

  • 此时,如果我想让他帮我更新一个字段为 null,就没有办法仅仅通过这个方法来完成

  • 因为 MyBatis-Plus 在设计的时候,给 null 赋值上了 “忽略,不作用” 的语义,而不是 “空值” 的语义(当然这是对的,不然写代码丢数据的风险就更高了)

  • 所以可以看到,null 可以被赋予更多的语义(由于刻意的设计)

再一个例子(假想的,每调研过业内该业务咋设计):

  • 我想表示一个状态机,拿微信好友 relation 业务举例,用一个 int 来表示我和另一个好友的关系
  • 如果数据库中暂不存在我和他 relation 行,则当作陌生人
  • 如果我申请添加他好友,则 int=1,他通过/拒绝 int=2/3,通过后我删除他/他删除我 int=4,5,通过后我拉黑他/他拉黑我 int=6,7
  • 如果是他申请的情况,则 int 分别为 8..14
  • 此时如果 int 可空,则可用 int=null 表示陌生人(诶,但我也可以不这样设计,我设计 int 是不可空字段,且 int=0 表示陌生人,此时 relation 行不存在和 int=0 是等价的)

总结

我的建议是:

  • 最好不要用 Optional(甚至在编码时当它不存在),尤其当你是一个没有很多编码经验,没有代码强迫症,没有好的 code taste 的程序员时(你刚看到了,我在年轻的时候写出过多少 shit 代码)
  • 如果你想用 Optional,最好在遇到我上述说到的第四种级别的场景中才用,并且只用到 local 代码中,不要去考虑设计到 field, parameter, return value 之类
  • 时常警惕 NPE

References


  Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×