9.6 CompletableFuture
这些问题的解决之道是CompletableFuture,它结合了Future对象打欠条的主意和使用回调处理事件驱动的任务。其要点是可以组合不同的实例,而不用担心末日金字塔问题。
你以前可能接触过CompletableFuture对象背后的概念,在其他语言中这被叫作延迟对象或约定。在Google Guava类库和Spring框架中,这被叫作ListenableFutures。
在例9-10中,我会使用CompletableFuture重写例9-9来展示它的用法。
例9-10 使用CompletableFuture从外部网站下载专辑信息
public Album lookupByName(String albumName) {CompletableFuture<List<Artist>> artistLookup= loginTo("artist").thenCompose(artistLogin -> lookupArtists(albumName, artistLogin)); ➊return loginTo("track").thenCompose(trackLogin -> lookupTracks(albumName, trackLogin)) ➋.thenCombine(artistLookup, (tracks, artists)-> new Album(albumName, tracks, artists)) ➌.join(); ➍}
在例9-10中,loginTo、lookupArtists和lookupTracks方法均返回 CompletableFuture ,而不是Future。CompletableFuture API的技巧是注册Lambda表达式,并且把高阶函数链接起来。方法不同,但道理和Stream API的设计是相通的。
在➊处使用thenCompose方法将Credentials对象转换成包含艺术家信息的CompletableFuture 对象,这就像和朋友借了点钱,然后在亚马逊上花了。你不会马上拿到新买的书——亚马逊会发给你一封电子邮件,告诉你新书正在运送途中,又是一张欠条!
在➋处还是使用了thenCompose方法,通过登录Track API,将Credentials对象转换成包含曲目信息的CompletableFuture 对象。这里引入了一个新方法thenCombine➌,该方法将一个CompletableFuture 对象的结果和另一个CompletableFuture 对象组合起来。组合操作是由用户提供的Lambda表达式完成,这里我们要使用曲目信息和艺术家信息构建一个Album对象。
这时我有必要提醒大家,和使用Stream API一样,现在还没真正开始做事呢,只是定义好了做事的规则。在调用最终的方法之前,无法保证CompletableFuture 对象已经生成结果。CompletableFuture 对象实现了Future接口,可以调用get方法获取值。CompletableFuture 对象包含join方法,我们在➍处调用了该方法,它的作用和get方法是一样的,而且它没有使用get方法时令人倒胃口的检查异常。
读者现在可能已经掌握了使用CompletableFuture 的基础,但是如何创建它们又是另外一回事。创建CompletableFuture 对象分两部分:创建对象和传给它欠客户代码的值。
如例9-11所示,创建CompletableFuture 对象非常简单,调用它的构造函数就够了。现在就可以将该对象传给客户代码,用来将操作链接在一起。我们同时保留了对该对象的引用,以便在另一个线程里继续执行任务。
例9-11 为Future提供值
CompletableFuture<Artist> createFuture(String id) {CompletableFuture<Artist> future = new CompletableFuture<>();startJob(future);return future;}
一旦任务完成,不管是在哪个线程里执行的,都需要告诉CompletableFuture对象那个值,这份工作可以由各种线程模型完成。比如,可以submit一个任务给ExecutorService,或者使用类似Vert.x这样基于事件循环的系统,或者直接启动一个线程来执行任务。在例9-12中,为了告诉CompletableFuture对象值已就绪,需要调用complete方法,是时候还债了,如图9-4所示。
例9-12 为Future提供一个值,完成工作
future.complete(artist);

图9-4:一个可完成的Future是一张可以被处理的欠条
当然,CompletableFuture的常用情境之一是异步执行一段代码,该段代码计算并返回一个值。为了避免大家重复实现同样的代码,有一个工厂方法supplyAsync,用来创建CompletableFuture实例,如例9-13所示。
例9-13 异步创建CompletableFuture实例的示例代码
CompletableFuture<Track> lookupTrack(String id) {return CompletableFuture.supplyAsync(() -> {// 这里会做一些繁重的工作 ➊// ...return track; ➋}, service); ➌}
supplyAsync方法接受一个Supplier对象作为参数,然后执行它。如➊处所示,这里的要点是能执行一些耗时的任务,同时不会阻塞当前线程——这就是方法名中Async的含义。➋处的返回值用来完成CompletableFuture。在➌处我们提供了一个叫作service的Executor,告诉CompletableFuture对象在哪里执行任务。如果没有提供Executor,就会使用相同的fork/join线程池并行执行。
当然,不是所有的欠条都能兑现。有时候碰上异常,我们无力偿还,如例9-14所示,CompletableFuture为此提供了completeExceptionally,用于处理异常情况。该方法可以视作complete方法的备选项,但不能同时调用complete和completeExceptionally方法。
例9-14 出现错误时完成Future
future.completeExceptionally(new AlbumLookupException("Unable to find " + name));
完整讨论CompletableFuture接口已经超出了本章的范围,很多时候它是一个隐藏大礼包。该接口有很多有用的方法,可以用你想到的任何方式组合CompletableFuture实例。现在,读者应该能熟练地使用高阶函数链接各种操作,告诉计算机应该做什么了吧?
让我们简单看一下其中的一些用例。
- 如果你想在链的末端执行一些代码而不返回任何值,比如
Consumer和Runnable,那就看看thenAccept和thenRun方法。 - 可使用
thenApply方法转换CompletableFuture对象的值,有点像使用Stream的map方法。 - 在
CompletableFuture对象出现异常时,可使用exceptionally方法恢复,可以将一个函数注册到该方法,返回一个替代值。 - 如果你想有一个
map,包含异常情况和正常情况,请使用handle方法。 - 要找出
CompletableFuture对象到底出了什么问题,可使用isDone和isCompletedExceptionally方法辅助调查。
CompletableFuture对于处理并发任务非常有用,但这并不是唯一的办法。下面要学习的概念提供了更多的灵活性,但是代码也更复杂。
