拉杰夫·辛格 • Java • 2017年7月18日 • 15分钟阅读
Java 8带来了大量的新功能和增强功能,例如Lambda表达式,Streams,CompletableFutures等。在本文中,我将通过简单的示例向您详细说明CompletableFuture及其所有方法。
什么是CompletableFuture?
CompletableFuture用于Java异步编程。异步编程是一种通过在与主应用程序线程不同的线程上运行任务,并向主线程通知其进度,完成或失败的方法来编写非阻塞代码的方法。
这样,您的主线程不会阻塞/等待任务完成,并且可以并行执行其他任务。
具有这种并行性可以大大提高程序的性能。
另请阅读: Java并发和多线程基础知识
Future vs CompletableFuture
CompletableFuture是Java 5中引入的Java Future API的扩展。
将来使用Future作为异步计算结果的参考。它提供了isDone()
一种检查计算是否完成的get()
方法,以及一种在计算完成后检索计算结果的方法。
您可以从我的Callable and Future Tutorial中了解有关Future的更多信息。
未来的API是迈向Java异步编程的重要一步,但缺少一些重要和有用的功能-
未来的局限性
-
无法手动完成:
假设您已经编写了一个从远程API获取电子商务产品最新价格的函数。由于此API调用很耗时,因此您需要在单独的线程中运行它,并从函数中返回Future。
现在,假设远程API服务已关闭,那么您想按产品的最后缓存价格手动完成Future。
你能用Future做到这一点吗?不!
-
您不能对Future的结果执行进一步的操作,而不会阻止:
未来不会将完成通知您。它提供了
get()
一种阻塞方法,直到结果可用为止。您无法将回调函数附加到Future上,并且当Future的结果可用时,该函数将自动被调用。
-
多个期货不能链接在一起:
有时您需要执行一个长时间运行的计算,当计算完成时,您需要将其结果发送到另一个长时间运行的计算,依此类推。
您无法使用Future创建此类异步工作流。
-
您不能将多个期货结合在一起:
假设您要并行运行10个不同的Future,然后在它们全部完成后再运行某些功能。对于Future,您也无法做到这一点。
-
无异常处理:
Future API没有任何异常处理构造。
哇!这么多限制吧?好吧,这就是我们拥有CompletableFuture的原因。您可以使用CompletableFuture实现以上所有目的。
CompletableFuture实现Future
并进行CompletionStage
接口,并提供了大量方便的方法来创建,链接和组合多个Future。它还具有非常全面的异常处理支持。
创建一个CompletableFuture
1.琐碎的例子-
您只需使用以下no-arg构造函数即可创建CompletableFuture-
CompletableFuture<String> completableFuture = new CompletableFuture<String>();
这是您可以拥有的最简单的CompletableFuture。所有想要获取此CompletableFuture结果的客户端都可以调用CompletableFuture.get()
方法-
String result = completableFuture.get()
该get()
方法将阻塞直到Future完成。因此,由于将来从未完成,因此上述调用将永远阻塞。
您可以使用CompletableFuture.complete()
方法手动完成Future-
completableFuture.complete("Future's Result")
所有等待此Future的客户端将获得指定的结果。并且,对的后续调用completableFuture.complete()
将被忽略。
2.使用runAsync()
-运行异步计算
如果要异步运行某些后台任务,并且不想从任务中返回任何内容,则可以使用CompletableFuture.runAsync()
method。它接受一个Runnable对象并返回CompletableFuture<Void>
。
// Run a task specified by a Runnable Object asynchronously.
CompletableFuture<Void> future = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
// Simulate a long-running Job
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("I'll run in a separate thread than the main thread.");
}
});
// Block and wait for the future to complete
future.get()
您还可以以lambda表达式的形式传递Runnable对象-
// Using Lambda Expression
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// Simulate a long-running Job
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("I'll run in a separate thread than the main thread.");
});
在本文中,我将非常频繁地使用lambda表达式,如果您还没有在Java代码中使用过lambda表达式,则也应该使用它。
3.异步运行任务,并使用supplyAsync()
-返回结果
CompletableFuture.runAsync()
对于不返回任何内容的任务很有用。但是,如果您想从后台任务返回一些结果怎么办?
好吧,CompletableFuture.supplyAsync()
是你的伴侣。它采用Supplier <T>并返回CompletableFuture<T>
,其中T是通过调用给定供应商获得的值的类型-
// Run a task specified by a Supplier object asynchronously
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of the asynchronous computation";
}
});
// Block and get the result of the Future
String result = future.get();
System.out.println(result);
甲供应商<T>是一个简单的功能接口表示结果的供应商。它只有一个get()
方法,您可以在其中编写后台任务并返回结果。
再一次,您可以使用Java 8的lambda表达式使上面的代码更简洁-
// Using Lambda Expression
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of the asynchronous computation";
});
关于执行器和线程池的说明-
您可能想知道-好吧,我知道
runAsync()
和supplyAsync()
方法在单独的线程中执行其任务。但是,我们从来没有创建线程对吗?是的!CompletableFuture在从全局ForkJoinPool.commonPool()获得的线程中执行这些任务。
但是,您也可以创建一个线程池,并将其传递给
runAsync()
和supplyAsync()
方法,以使它们在从您的线程池获得的线程中执行任务。CompletableFuture API中的所有方法都有两个变体-一个变体接受一个Executor作为参数,另一个变体不接受-
// Variations of runAsync() and supplyAsync() methods static CompletableFuture<Void> runAsync(Runnable runnable) static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
这是创建线程池并将其传递给以下方法之一的方法-
Executor executor = Executors.newFixedThreadPool(10); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new IllegalStateException(e); } return "Result of the asynchronous computation"; }, executor);
转换并作用于CompletableFuture
该CompletableFuture.get()
方法正在阻止。它等待,直到Future完成,并在其完成后返回结果。
但是,那不是我们想要的吗?对于构建异步系统,我们应该能够将回调附加到CompletableFuture上,当Future完成时,该回调应自动被调用。
这样,我们就不必等待结果了,我们可以在回调函数中编写Future完成后需要执行的逻辑。
您可以使用附加回调到CompletableFuture thenApply()
,thenAccept()
和thenRun()
方法-
1. thenApply()
您可以使用thenApply()
方法来处理和转换CompletableFuture的结果。它以Function <T,R>作为参数。Function <T,R>是一个简单的函数接口,表示一个函数,该函数接受类型T的参数并产生类型R的结果-
// Create a CompletableFuture
CompletableFuture<String> whatsYourNameFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Rajeev";
});
// Attach a callback to the Future using thenApply()
CompletableFuture<String> greetingFuture = whatsYourNameFuture.thenApply(name -> {
return "Hello " + name;
});
// Block and get the result of the future.
System.out.println(greetingFuture.get()); // Hello Rajeev
您还可以通过附加一系列回调方法在CompletableFuture上编写一系列转换thenApply()
。一种thenApply()
方法的结果传递到系列中的下一个-
CompletableFuture<String> welcomeText = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Rajeev";
}).thenApply(name -> {
return "Hello " + name;
}).thenApply(greeting -> {
return greeting + ", Welcome to the CalliCoder Blog";
});
System.out.println(welcomeText.get());
// Prints - Hello Rajeev, Welcome to the CalliCoder Blog
2. thenAccept()和thenRun()
如果您不想从回调函数返回任何内容,而只想在Future完成后运行一些代码,则可以使用thenAccept()
和thenRun()
方法。这些方法是使用者,通常用作回调链中的最后一个回调。
CompletableFuture.thenAccept()
接受Consumer <T>并返回CompletableFuture<Void>
。它可以访问其所附加的CompletableFuture的结果。
// thenAccept() example
CompletableFuture.supplyAsync(() -> {
return ProductService.getProductDetail(productId);
}).thenAccept(product -> {
System.out.println("Got product detail from remote service " + product.getName())
});
虽然thenAccept()
可以访问其所连接的CompletableFuture的结果,thenRun()
但甚至无法访问Future的结果。需要aRunnable
并返回CompletableFuture<Void>
-
// thenRun() example
CompletableFuture.supplyAsync(() -> {
// Run some computation
}).thenRun(() -> {
// Computation Finished.
});
关于异步回调方法的说明-
CompletableFuture提供的所有回调方法都有两个异步变体-
// thenApply() variants <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
这些异步回调变体可通过在单独的线程中执行回调任务来帮助您进一步并行化计算。
考虑以下示例-
CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new IllegalStateException(e); } return "Some Result" }).thenApply(result -> { /* Executed in the same thread where the supplyAsync() task is executed or in the main thread If the supplyAsync() task completes immediately (Remove sleep() call to verify) */ return "Processed Result" })
在上述情况下,内部任务在
thenApply()
执行任务的同一线程中supplyAsync()
执行,或者如果supplyAsync()
任务立即完成(尝试删除sleep()
调用以进行验证),则在主线程中执行。要对执行回调任务的线程有更多控制,可以使用异步回调。如果使用
thenApplyAsync()
回调,则它将在从ForkJoinPool.commonPool()
-获得的不同线程中执行CompletableFuture.supplyAsync(() -> { return "Some Result" }).thenApplyAsync(result -> { // Executed in a different thread from ForkJoinPool.commonPool() return "Processed Result" })
此外,如果将Executor传递给
thenApplyAsync()
回调,则该任务将在从Executor的线程池获得的线程中执行。Executor executor = Executors.newFixedThreadPool(2); CompletableFuture.supplyAsync(() -> { return "Some result" }).thenApplyAsync(result -> { // Executed in a thread obtained from the executor return "Processed Result" }, executor);
将两个CompletableFutures组合在一起
1.使用thenCompose()合并两个相依的期货-
假设您要从远程API服务中获取用户的详细信息,并且一旦该用户的详细信息可用,就希望从另一服务中获取其信用等级。
考虑以下实现getUserDetail()
和getCreditRating()
方法-
CompletableFuture<User> getUsersDetail(String userId) {
return CompletableFuture.supplyAsync(() -> {
return UserService.getUserDetails(userId);
});
}
CompletableFuture<Double> getCreditRating(User user) {
return CompletableFuture.supplyAsync(() -> {
return CreditRatingService.getCreditRating(user);
});
}
现在,让我们了解一下,如果我们使用它thenApply()
来达到期望的结果会发生什么-
CompletableFuture<CompletableFuture<Double>> result = getUserDetail(userId)
.thenApply(user -> getCreditRating(user));
在较早的示例中,Supplier
传递给thenApply()
回调的函数将返回一个简单值,但在这种情况下,它将返回CompletableFuture。因此,上述情况下的最终结果是嵌套的CompletableFuture。
如果您希望最终结果是顶级的Future,请使用thenCompose()
方法-
CompletableFuture<Double> result = getUserDetail(userId)
.thenCompose(user -> getCreditRating(user));
因此,这里有一个经验法则-如果您的回调函数返回CompletableFuture,并且您希望从CompletableFuture链中获得平坦的结果(在大多数情况下,您会希望这样做),请使用thenCompose()
。
2.使用thenCombine()合并两个独立的期货-
WhilethenCompose()
用于合并两个期货,其中一个期货依赖于另一个thenCombine()
期货,而当您希望两个期货独立运行并在两个期货都完成之后执行某项操作时,将使用while 。
System.out.println("Retrieving weight.");
CompletableFuture<Double> weightInKgFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return 65.0;
});
System.out.println("Retrieving height.");
CompletableFuture<Double> heightInCmFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return 177.8;
});
System.out.println("Calculating BMI.");
CompletableFuture<Double> combinedFuture = weightInKgFuture
.thenCombine(heightInCmFuture, (weightInKg, heightInCm) -> {
Double heightInMeter = heightInCm/100;
return weightInKg/(heightInMeter*heightInMeter);
});
System.out.println("Your BMI is - " + combinedFuture.get());
thenCombine()
当两个期货均完成时,将调用传递给的回调函数。
将多个CompletableFutures组合在一起
我们使用thenCompose()
和thenCombine()
两个CompletableFutures结合在了一起。现在,如果要组合任意数量的CompletableFutures怎么办?好吧,您可以使用以下方法来组合任意数量的CompletableFutures-
static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
1. CompletableFuture.allOf()
CompletableFuture.allOf
当您具有要并行运行的独立期货列表并在所有期货都完成后要执行某项操作时,使用此脚本。
假设您要下载网站的100个不同网页的内容。您可以按顺序执行此操作,但这将花费很多时间。因此,您编写了一个函数,该函数带有一个网页链接,并返回一个CompletableFuture,即它异步下载该网页的内容-
CompletableFuture<String> downloadWebPage(String pageLink) {
return CompletableFuture.supplyAsync(() -> {
// Code to download and return the web page's content
});
}
现在,下载所有网页后,您要计算包含关键字“ CompletableFuture”的网页数。让我们CompletableFuture.allOf()
来实现这一目标-
List<String> webPageLinks = Arrays.asList(...) // A list of 100 web page links
// Download contents of all the web pages asynchronously
List<CompletableFuture<String>> pageContentFutures = webPageLinks.stream()
.map(webPageLink -> downloadWebPage(webPageLink))
.collect(Collectors.toList());
// Create a combined Future using allOf()
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
pageContentFutures.toArray(new CompletableFuture[pageContentFutures.size()])
);
问题CompletableFuture.allOf()
在于它返回了CompletableFuture<Void>
。但是我们可以通过编写一些额外的代码行来获取所有包装的CompletableFuture的结果-
// When all the Futures are completed, call `future.join()` to get their results and collect the results in a list -
CompletableFuture<List<String>> allPageContentsFuture = allFutures.thenApply(v -> {
return pageContentFutures.stream()
.map(pageContentFuture -> pageContentFuture.join())
.collect(Collectors.toList());
});
花一点时间来理解上面的代码片段。因为我们future.join()
在所有期货都完成后才打电话,所以我们不会在任何地方封锁:-)
该join()
方法类似于get()
。唯一的区别是,如果基础CompletableFuture异常完成,它将引发未经检查的异常。
现在,让我们计算包含我们的关键字的网页数量-
// Count the number of web pages having the "CompletableFuture" keyword.
CompletableFuture<Long> countFuture = allPageContentsFuture.thenApply(pageContents -> {
return pageContents.stream()
.filter(pageContent -> pageContent.contains("CompletableFuture"))
.count();
});
System.out.println("Number of Web Pages having CompletableFuture keyword - " +
countFuture.get());
2. CompletableFuture.anyOf()
CompletableFuture.anyOf()
顾名思义,它返回一个新的CompletableFuture,当给定的CompletableFuture中的任何一个完成时,该新的CompletableFuture将完成,并且结果相同。
考虑以下示例-
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 2";
});
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 3";
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);
System.out.println(anyOfFuture.get()); // Result of Future 2
在上面的示例中,anyOfFuture
当三个CompletableFuture中的任何一个完成时,完成。由于future2
睡眠时间最短,它将首先完成,最终结果将是-Future 2的结果。
CompletableFuture.anyOf()
接受期货和回报的变量CompletableFuture<Object>
。问题CompletableFuture.anyOf()
在于,如果您拥有返回不同类型结果的CompletableFuture,那么您将不知道最终CompletableFuture的类型。
CompletableFuture异常处理
我们探讨了如何创建CompletableFuture,对其进行转换以及组合多个CompletableFuture。现在,让我们了解发生任何问题时该怎么办。
首先让我们了解错误如何在回调链中传播。考虑以下CompletableFuture回调链-
CompletableFuture.supplyAsync(() -> {
// Code which might throw an exception
return "Some result";
}).thenApply(result -> {
return "processed result";
}).thenApply(result -> {
return "result after further processing";
}).thenAccept(result -> {
// do something with the final result
});
如果原始supplyAsync()
任务中发生错误,则不会thenApply()
调用任何回调,并且将在发生异常的情况下解决将来的问题。如果在第一个thenApply()
回调中发生错误,则不会调用第二个和第三个回调,并且将在发生异常的情况下解决将来的问题,依此类推。
1.使用exception()回调处理异常
该exceptionally()
回调给你一个机会,从最初的未来产生的错误中恢复。您可以在此处记录异常并返回默认值。
Integer age = -1;
CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).exceptionally(ex -> {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
});
System.out.println("Maturity : " + maturityFuture.get());
请注意,如果您处理一次,该错误将不会在回调链中进一步传播。
2.使用通用的handle()方法处理异常
该API还提供了一种更通用的方法-handle()
从异常中恢复。称为是否发生异常。
Integer age = -1;
CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).handle((res, ex) -> {
if(ex != null) {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
}
return res;
});
System.out.println("Maturity : " + maturityFuture.get());
如果发生异常,则res
参数将为null,否则,ex
参数将为null。
结论
恭喜大家!在本教程中,我们探讨了CompletableFuture API的最有用和最重要的概念。