1. 概述
在这个教程中,我们将探讨Java 8中的Callable
和Supplier
功能性接口,它们在结构上相似但用途不同。两者都返回一个类型化的值,不接受任何参数。执行上下文是区分它们的关键。
本文将重点放在异步任务的上下文中。
2. 模型
在开始之前,我们先定义一个类:
public class User {
private String name;
private String surname;
private LocalDate birthDate;
private Integer age;
private Boolean canDriveACar = false;
// standard constructors, getters and setters
}
3. Callable
Callable
是在Java 5版本中引入的,到Java 8时演变成一个功能性接口。
它的SAM(单一抽象方法)是call()
,它返回一个泛型值并可能抛出异常:
V call() throws Exception;
它的设计目的是封装由其他线程执行的任务,例如Runnable
接口。因此,Callable
实例可以通过ExecutorService
来执行。
现在让我们定义一个实现:
public class AgeCalculatorCallable implements Callable<Integer> {
private final LocalDate birthDate;
@Override
public Integer call() throws Exception {
return Period.between(birthDate, LocalDate.now()).getYears();
}
// standard constructors, getters and setters
}
当call()
方法返回一个值时,主线程会获取它来执行其逻辑。为此,我们可以使用Future
,这是一个对象,用于跟踪并在其他线程执行的任务完成后获取值。
3.1. 单个任务
让我们定义一个只执行一个异步任务的方法:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
Future<Integer> ageFuture = executorService.submit(new AgeCalculatorCallable(user.getBirthDate()));
user.setAge(ageFuture.get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e.getCause());
}
return user;
}
我们可以使用lambda表达式重写内部的submit()
块:
Future<Integer> ageFuture = executorService.submit(
() -> Period.between(user.getBirthDate(), LocalDate.now()).getYears());
当我们尝试通过调用get()
方法访问返回值时,我们需要处理两种受检异常:
-
InterruptedException
:当线程在睡眠、活动或占用时发生中断时抛出。 -
ExecutionException
:当任务因抛出异常而被取消。换句话说,这是一个包装异常,真正导致任务失败的异常是原因(并且可以使用getCause()
方法检查)。
3.2. 任务链
任务链的执行依赖于前一任务的状态。如果其中一个失败,当前任务无法执行。
所以,让我们定义一个新的Callable
:
public class CarDriverValidatorCallable implements Callable<Boolean> {
private final Integer age;
@Override
public Boolean call() throws Exception {
return age > 18;
}
// standard constructors, getters and setters
}
接下来,让我们定义一个任务链,其中第二个任务的输入参数是前一个任务的结果:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
Future<Integer> ageFuture = executorService.submit(new AgeCalculatorCallable(user.getBirthDate()));
Integer age = ageFuture.get();
Future<Boolean> canDriveACarFuture = executorService.submit(new CarDriverValidatorCallable(age));
Boolean canDriveACar = canDriveACarFuture.get();
user.setAge(age);
user.setCanDriveACar(canDriveACar);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e.getCause());
}
return user;
}
在任务链中使用Callable
和Future
存在一些问题:
- 链中的每个任务都遵循“submit-get”模式。在长链中,这会产生冗长的代码。
- 当链对任务失败具有容错性时,应创建专门的
try-catch
块。 - 调用
get()
方法时,它会等待Callable
返回一个值。因此,链中所有任务执行时间之和等于整个链的总执行时间。但如果下一个任务仅依赖于一个前一个任务的正确执行,那么链的处理速度会显著降低。
4. Supplier
Supplier
是一个功能性接口,其SAM(单一抽象方法)是get()
。
它不接受任何参数,返回一个值,且只抛出未检查异常:
T get();
这个接口最常见的用例之一是延迟执行某些代码。
Optional
类有几个方法接受Supplier
作为参数,如Optional.or()
、Optional.orElseGet()
等。
因此,Supplier
只在Optional
为空时执行。
我们也可以在异步计算上下文中使用它,特别是在CompletableFuture
API中。
有些方法接受Supplier
作为参数,比如supplyAsync()
方法。
4.1. 单个任务
让我们定义一个只执行一个异步任务的方法:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<Integer> ageFut = CompletableFuture.supplyAsync(() -> Period.between(user.getBirthDate(), LocalDate.now())
.getYears(), executorService)
.exceptionally(throwable -> {throw new RuntimeException(throwable);});
user.setAge(ageFut.join());
return user;
}
在这种情况下,lambda表达式定义了Supplier
,但我们也可以定义一个实现类。借助CompletableFuture
,我们为异步操作定义了一个模板,使其更易于理解和修改。join()
方法提供Supplier
的返回值。
4.2. 任务链
我们还可以借助Supplier
接口和CompletableFuture
构建任务链:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<Integer> ageFut = CompletableFuture.supplyAsync(() -> Period.between(user.getBirthDate(), LocalDate.now())
.getYears(), executorService);
CompletableFuture<Boolean> canDriveACarFut = ageFut.thenComposeAsync(age -> CompletableFuture.supplyAsync(() -> age > 18, executorService))
.exceptionally((ex) -> false);
user.setAge(ageFut.join());
user.setCanDriveACar(canDriveACarFut.join());
return user;
}
使用CompletableFuture
和Supplier
方法定义任务链可能解决之前使用Future
和Callable
方法时出现的一些问题:
- 链中的每个任务都是独立的。如果任务执行失败,我们可以通过
exceptionally()
块进行处理。 -
join()
方法不需要在编译时处理受检异常。 - 我们可以设计一个异步任务模板,改进每个任务的状态处理。
5. 总结
在这篇文章中,我们讨论了Callable
和Supplier
接口之间的差异,重点关注异步任务的上下文。
主要的接口设计差异在于Callable
抛出的受检异常。
Callable
最初并不是为函数式上下文设计的,随着时间的推移,它逐渐适应了,但函数编程和受检异常并不兼容。
因此,任何功能性的API(如CompletableFuture
API)总是接受Supplier
而不是Callable
。
如往常一样,示例的完整源代码可以在GitHub上找到。