跳到正文
hello world

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,但是它们执行线程完全不一样。理解了这一点,后面看源码和排查线程问题都会轻松很多。

33. thenApply、thenAccept、thenRun 到底有什么区别 - Java并发之从Thread到CompletableFuture - 上下文网