Java Optional使用的最佳实践
作者丨安琪拉
来源丨安琪拉的博客
我们经常在编程的遇到需要做空判断的场景。
例如拉哥最近遇到的一个场景,需要获取任务节点的执行完成时间,是Date类型的,但是上游需要时间的毫秒,所以写了这么一段代码
public Result<TaskInfoVo> getTaskInfo(String taskId){
TaskNode taskNode = taskExecutor.getByTaskId(String taskId);
//返回任务视图
TaskInfoVo taskInfoVo = TaskInfoVo
.builder()
.taskName(taskNode.getName())
.finishTime(taskNode.getFinishTime().getTime())
.user(taskNode.getUser())
.memo(taskNode.getMemo())
.build()));;
return Result.ok(taskInfoVo);
}
class TaskInfoVo {
/**
** 完成时间
**/
Long finishTime;
}
但是运行时发现任务的执行时间可能为null,taskNode.getFinishTime().getTime() 这里会产生NPE(空指针异常)。
如果不使用 Optional,一般判空的方式可以这么写:
//第一种判空
if (Objects.notNull(taskNode.getFinishTime())) {
taskInfoVo.set(taskNode.getFinishTime().getTime());
}
//第二种判空 保留builder模式
TaskInfoVo
.builder()
.finishTime(taskNode.getFinishTime() == null ? null : taskNode.getFinishTime().getTime())
.build()));
第一种方式就不能使用builder模式,值的设置割裂开了。
第二种方式采用三目表达式也还算清晰,只是执行了二次 getFinishTime(),如果在不使用Optional的二种方式,更推荐第二种,清晰一致。
再说第三种,使用Optional 方式。如下所示:
public Result<TaskInfoVo> getTaskInfo(String taskId){
TaskNode taskNode = taskExecutor.getByTaskId(String taskId);
//返回任务视图
TaskInfoVo taskInfoVo = TaskInfoVo
.builder()
.taskName(taskNode.getName())
.finishTime(Optional.ofNullable(taskNode.getFinishTime()).map(date -> date.getTime()).orElse(null))
.user(taskNode.getUser())
.memo(taskNode.getMemo())
.build()));;
return Result.ok(taskInfoVo);
}
首先,我们使用 Optional.ofNullable
把可能为空的值包了一层,然后用map和orElse 来设置存在值和为空分别的结果。
我们来看看Optional 内部的实现细节,代码很清晰,也很简单。
/**
**@since 1.8 jdk1.8引入
**/
public final class Optional<T> { // final 修饰,不能被继承,也就是不允许重写
/**
* Common instance for {@code empty()}. 空对象,但注意,不是null
*/
private static final Optional<?> EMPTY = new Optional<>();
/**
* 存储的值
*/
private final T value;
private Optional() {
this.value = null;
}
//空数据
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
//带参构造函数
private Optional(T value) {
//value 不能为空
this.value = Objects.requireNonNull(value);
}
//一般不建议使用of,因为传入的值不允许为空,否则抛异常
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
//一般确定value不为null,才调用get
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
//是否存在 present是个好词,源码经常用
public boolean isPresent() {
return value != null;
}
//如果存在,调用consumer 消费value值
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
//判断,predicate有test函数,filter返回过滤后Optional
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
//映射转化 mapper
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
//和map相比,flatMap返回结果不能为空,否则抛NPE
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
//值不为空 返回 否则返回其他
public T orElse(T other) {
return value != null ? value : other;
}
//值不为空,返回,否则返回传入的其他值
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
//值不为空,直接返回,否则丢出提供的异常
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
}
//列出了 Objects
class Objects {
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
}
我们来分别看一下这些方法的使用场景
-
希望为空时获得默认值
Task defalutTask = new Task("defalutTask", defalutTaskInfo);
return Optional.ofNullable(task).orElse(defalutTask); -
希望为空时进行函数求值
Task defalutTask = new Task("defalutTask", defalutTaskInfo);
return Optional.ofNullable(task).orElseGet(() -> assembleDefaultTask());
private Task assembleDefaultTask() {
Task currentTask = ExecutorManager.getCurrentTaskFromContext();
currentTask.reset();
return currentTask;
}orElseGet 与 orElse 的区别在于 orElseGet 的参数是个 Supplier 对象,orElse 是值。
Supplier 是java8 引入的。Supplier 接口仅包含一个无参的方法: T get() 。用来获取一个泛型参数指定类型的对象数据。
.orElseGet(() -> assembleDefaultTask());
-
空判断
if (taskOptional.isPresent()) {
//doSomeThing();
} -
如果存在,执行下一步消费者动作。
taskOptional.ifPresent([Consumer]<? super [T]> consumer)
//还是以日期处理为例
Optional.ofNullable(previewStep.getFinishTime()).ifPresent(date -> processPreviewVO.setFinishTime(date.getTime()));消费者大家不陌生,就是执行一套消费动作,lamada 的写法简化了代码,但是降低了可读性,
date -> processPreviewVO.setFinishTime(date.getTime())
实际上就是这段代码:new Consumer<Date>() {
@Override
public void accept(Date date) {
processPreviewVO.setFinishDate(date.getTime());
}
} -
orElse() 和 orElseGet() 的不同之处
User user = null;
logger.debug("Using orElse");
User result = Optional.ofNullable(user).orElse(createNewUser());
logger.debug("Using orElseGet");
User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
private User createNewUser() {
logger.debug("Creating New User");
return new User("extra@gmail.com", "1234");
}输出:
Using orElse
Creating New User
Using orElseGet
Creating New User如果user 为null 时结果是一致的;
但是user 不为null 时,行为是不同的,我们来看下
User user = new User("john@gmail.com", "1234");
logger.info("Using orElse");
User result = Optional.ofNullable(user).orElse(createNewUser());
logger.info("Using orElseGet");
User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());输出:
Using orElse
Creating New User
Using orElseGet为什么呢?
因为orElse ( T value) 函数入参是普通变量,因为会将函数计算好的结果作为参数传进去。
但是orElseGet(Supplier<? extends T> other)入参实际上是个 Supplier 对象, 因为只有一个方法,所以可以用lambda写法。
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}对象的函数没有被调用是不会自己主动执行的。
-
值转化
我们先来看map的例子,实际上面已经演示过了,下面再讲解一下。
User user = new User("angela@gmail.com", "niubi");
String email = Optional.ofNullable(user)
.map(u -> u.getEmail()).orElse("default@gmail.com");map可以对结果进行转化,返回的是依然是Optional,方便你后续的链式调用。
-
值过滤
User user = new User("angela@gmail.com", "666");
Optional<User> result = Optional.ofNullable(user)
.filter(u -> u.getEmail() != null && u.getEmail().contains("@"));这个filter 方法实际上类似断言,我总觉得对集合元素进行遍历,判断是否符合预期才更适合叫做 filter ,这个功能用的比较少。
-
Optional类的链式方法
比如现在我们有这个一个场景,智能游戏机器人需要让安琪拉释放火球技能,它需要判断自己英雄池是否有安琪拉,并且安琪拉火球技能是否ready。
代码实现:
String result = Optional.ofNullable(heroPool)
.flatMap(heroPool -> heroPool.getHero("angela"))
.flatMap((Angela)hero -> hero.getFireBall())
.map(c -> c.fire())
.orElse("failure");如果用常规代码,需要做很多层判空处理。
-
注意事项
-
不要将null赋给Optional 虽然Optional支持null值,但是不要显示的把null 传递给Optional
-
尽量避免使用Optional.get()
-
当结果不确定是否为null时,且需要对结果做下一步处理,使用Optional;
-
在类、集合中尽量不要使用Optional 作为基本元素;
-
尽量不要在方法参数中传递Optional;
-
大胆使用map、filter 重构代码
//1. map 示例
if ( hero != null){
return "hero : " + hero.getName() + " is fire...";
} else {
return "angela";
}
//重构成
String heroName = hero
.map(this::printHeroName)
.orElseGet(this::getDefaultName);
public void printHeroName(Hero dog){
return "hero : " + hero.getName() + " is fire...";
}
public void getDefaultName(){
return "angela";
}
//2. filter示例
Hero hero = fetchHero();
if(hero != null && hero.hasBlueBuff()){
hero.fire();
}
//重构成
Optional<Hero> optionalHero = fetchHero();
optionalHero
.filter(Hero::hasBlueBuff)
.ifPresent(this::fire); -
不要使用 Optional 作为Java Bean Setter方法的参数
因为Optional 是不可序列化的,而且降低了可读性。
-
不要使用Optional作为Java Bean实例域的类型
原因同上。
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取