33. thenApply、thenAccept、thenRun 到底有什么区别
发布于 • 阅读量 0
33. thenApply、thenAccept、thenRun 到底有什么区别
学 CompletableFuture 的时候,我觉得最容易混淆的就是这三个方法:
thenApply(...)
thenAccept(...)
thenRun(...)
第一次看到的时候,它们长得太像了。
都是:
future.thenXXX(...)
但是实际用下来,我发现它们解决的是三个不同的问题。
其实只要记住一句话就够了:
thenApply:拿结果,加工结果,再返回一个新结果。
thenAccept:拿结果,做点事情,不返回结果。
thenRun:不关心结果,只要前面的任务完成了,就执行下一步。
这三个方法,我现在基本不会混了。
一张图先记住
可以先看这个流程。
假设前面有一个 PDF 水印任务:
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
return "output/a.pdf";
});
返回的是:
output/a.pdf
后面三种写法分别是:
thenApply
future.thenApply(path -> {
return "http://localhost/" + path;
});
结果:
output/a.pdf
↓
http://localhost/output/a.pdf
它返回了一个新的值。
thenAccept
future.thenAccept(path -> {
System.out.println(path);
});
结果:
拿到 output/a.pdf
↓
打印日志
↓
没有返回值
thenRun
future.thenRun(() -> {
System.out.println("全部完成");
});
结果:
前面的任务完成
↓
执行这一段
↓
不知道前面的结果是什么
注意:
这里已经拿不到 path 了。
为什么要分三个方法
如果只有一个 then() 不行吗?
其实也可以设计。
但是 Java 把三种需求拆开以后,可读性更好。
现实项目里,这三种场景出现得非常多。
例如:
PDF 水印:
处理 PDF
↓
得到输出路径
↓
生成下载地址
↓
保存数据库
↓
发送通知
这里每一步做的事情其实都不一样。
所以对应的方法也不同。
一、thenApply——我要把结果变成另一个结果
这是我用得最多的方法。
比如:
PDF 处理完成以后:
返回:
output/a.pdf
但是前端真正需要的是:
http://localhost/download?file=output/a.pdf
也就是说:
我要把:
路径
↓
下载地址
这就是典型的 thenApply。
例如:
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(() -> {
sleep(3000);
return "output/a.pdf";
})
.thenApply(path -> {
return "http://localhost/download?file=" + path;
});
String url = future.join();
System.out.println(url);
流程就是:
output/a.pdf
↓
thenApply
↓
http://localhost/download?file=output/a.pdf
所以:
thenApply:
输入一个结果
↓
加工一下
↓
返回新的结果
再举一个例子
假设:
返回的是:
a.pdf
我要把它变成:
A.PDF
可以写:
future.thenApply(name -> name.toUpperCase());
或者:
返回:
10
我要平方:
future.thenApply(num -> num * num);
结果就是:
10
↓
100
只要:
结果发生变化
一般就是 thenApply。
二、thenAccept——我要用结果,但是不用返回
比如:
PDF 处理完成以后。
我要:
保存数据库。
代码:
future.thenAccept(path -> {
System.out.println("保存数据库:" + path);
});
这里:
拿到了:
output/a.pdf
但是:
没有返回新的值。
因为:
保存数据库以后,
流程就结束了。
所以:
thenAccept:
消费结果
↓
结束
它的返回类型是:
CompletableFuture<Void>
因为:
已经没有结果了。
日志也是 thenAccept
例如:
future.thenAccept(path -> {
System.out.println(path);
});
或者:
future.thenAccept(result -> {
saveToDatabase(result);
});
或者:
future.thenAccept(result -> {
sendMessage(result);
});
这些都是:
拿结果
↓
做事情
↓
没有新的结果
所以都是 thenAccept。
三、thenRun——我连结果都不要
thenRun 更简单。
例如:
PDF 已经处理完成。
我只是想打印一句:
全部完成
根本不用知道:
输出路径是什么。
可以:
future.thenRun(() -> {
System.out.println("全部完成");
});
注意:
这里没有参数。
因为:
thenRun 根本不知道:
前面返回了什么。
它只知道:
前面的 CompletableFuture
结束了。
所以:
thenRun:
什么都不要
↓
只执行
一个完整例子
新建:
ThenDemo
代码:
CompletableFuture
.supplyAsync(() -> {
System.out.println("生成 PDF");
sleep(3000);
return "output/a.pdf";
})
.thenApply(path -> {
System.out.println("生成下载地址");
return "http://localhost/" + path;
})
.thenAccept(url -> {
System.out.println("通知用户:" + url);
})
.thenRun(() -> {
System.out.println("全部流程结束");
})
.join();
执行顺序:
生成 PDF
↓
生成下载地址
↓
通知用户
↓
全部流程结束
这就是一个非常典型的异步流水线。
为什么 thenApply 能一直链下去
例如:
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(() -> "a.pdf")
.thenApply(name -> name.toUpperCase())
.thenApply(name -> "output/" + name)
.thenApply(path -> "http://localhost/" + path);
为什么可以一直写?
因为:
thenApply:
返回的还是:
CompletableFuture<T>
所以:
还能继续 thenApply。
例如:
a.pdf
↓
A.PDF
↓
output/A.PDF
↓
http://localhost/output/A.PDF
一直都是:
输入
↓
加工
↓
输出
所以特别适合:
流水线。
thenAccept 为什么不能继续 thenApply
例如:
future
.thenAccept(path -> {
System.out.println(path);
})
这里:
已经没有结果了。
因为:
thenAccept 返回:
CompletableFuture<Void>
所以:
后面如果:
.thenApply(...)
就拿不到:
path。
因为:
结果已经被消费掉了。
PDF 实战应该怎么写
如果按照真实业务。
我现在更喜欢这种结构:
CompletableFuture
.supplyAsync(() -> {
return addWatermark(file);
}, pdfExecutor)
.thenApply(path -> {
return buildDownloadUrl(path);
})
.thenAccept(url -> {
saveDatabase(url);
})
.thenRun(() -> {
System.out.println("当前 PDF 全部完成");
});
是不是就特别像业务流程?
几乎不用写注释。
什么时候不要用 thenRun
例如:
你需要:
输出路径。
千万不要:
future.thenRun(() -> {
save(path);
});
因为:
这里:
根本没有:
path。
很多人第一次写都会踩这个坑。
只要:
后面的步骤:
需要前面的返回值。
永远:
不要写:
thenRun。
应该写:
thenApply
或者
thenAccept
我现在怎么区分这三个方法
我已经不去死记 API。
而是先问自己三个问题。
第一:
我要不要返回新的结果?
如果:
要。
就是:
thenApply
第二:
我要不要拿到前面的结果?
如果:
要。
但是:
不用返回。
就是:
thenAccept
第三:
我连结果都不要。
只是:
等前面结束。
就是:
thenRun
这样基本不会选错。
这一节小结
这一节我主要记住几点:
1、thenApply:接收结果,加工结果,返回新的结果。
2、thenAccept:接收结果,执行操作,不返回结果。
3、thenRun:不接收结果,只关心前面的任务是否完成。
4、thenApply 返回 CompletableFuture<T>,适合继续组成流水线。
5、thenAccept 返回 CompletableFuture<Void>,表示结果已经消费。
6、thenRun 最简单,只表示"完成以后执行下一步"。
7、真实项目里,大多数业务流程都会组合使用这三个方法。
我现在对它们的记忆方式只有一句话:
要改结果,用 thenApply;
要用结果,用 thenAccept;
不要结果,用 thenRun。
下一节我们继续学习 带 Async 和不带 Async 的区别。
这是 CompletableFuture 最容易踩坑的地方之一。
例如:
thenApply()
thenApplyAsync()
只有一个 Async,但是它们执行线程完全不一样。理解了这一点,后面看源码和排查线程问题都会轻松很多。