引言
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> { private static final java.util.Optional<?> EMPTY = new java.util.Optional<>(); private final T value; private Optional() ; public static<T> java.util.Optional<T> empty() ; private Optional(T value); public static <T> java.util.Optional<T> of(T value); public static <T> java.util.Optional<T> ofNullable(T value) ; public T get() ; public boolean isPresent(); public void ifPresent(Consumer<? super T> consumer) ; public java.util.Optional<T> filter(Predicate<? super T> predicate) ; public<U> java.util.Optional<U> map(Function<? super T, ? extends U> mapper); public<U> java.util.Optional<U> flatMap(Function<? super T, java.util.Optional<U>> mapper) ; public T orElse(T other); public T orElseGet(Supplier<? extends T> 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.orElse
和 Optional.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
| 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 非法"); } });
if (StringUtils.isNotEmpty(reqDTO.getProblemCode())) { try { reqDTO.setProblemIndex(Integer.parseInt(reqDTO.getProblemCode())); } catch (Exception e) { throw new ApiException(ApiExceptionEnum.PARAMETER_ERROR, "problemCode 非法"); } }
Optional.ofNullable(problemCodeToMetrics.get(p.getProblemCode())).ifPresent(metrics -> { p.setAcceptNum(metrics.getAcceptNum()); p.setSubmitNum(metrics.getSubmitNum()); });
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
| Optional.ofNullable(redisPassword) .filter(StringUtils::isNotEmpty) .map(RedisPassword::of) .ifPresent(configuration::setPassword);
if (StringUtils.isNotEmpty(redisPassword)) { configuration.setPassword(RedisPassword.of(redisPassword)); }
public Integer getMaxMemoryFactor() { return Optional.ofNullable(maxMemoryFactor).orElse(1); }
public Integer getMaxMemoryFactor() { return maxMemoryFactor != null ? maxMemoryFactor : 1; }
Long userId = Optional.ofNullable(userSessionDTO).map(UserSessionDTO::getUserId).orElse(null);
Long userId = userSessionDTO != null ? userSessionDTO.getUserId() : null;
Optional.ofNullable(reqDTO.getContestId()) .ifPresent(contestId -> query.eq(SubmissionDO::getContestId, contestId));
if (reqDTO.getContestId() != null) { query.eq(SubmissionDO::getContestId, reqDTO.getContestId()); }
String emailCode = Optional.ofNullable((String) redisUtils.get(redisKey)).orElseGet(() -> CaptchaUtils.getRandomString(6));
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
| 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) { oneBoolean = false; } }
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
| 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; }
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); }
String version = "UNKNOWN"; if(computer != null){ Soundcard soundcard = computer.getSoundcard(); if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null){ version = usb.getVersion(); } } }
String name = Optional.ofNullable(computer) .map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");
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