1. 概述

在这个教程中,我们将探讨Java 8中的CallableSupplier功能性接口,它们在结构上相似但用途不同。两者都返回一个类型化的值,不接受任何参数。执行上下文是区分它们的关键。

本文将重点放在异步任务的上下文中。

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;
}

在任务链中使用CallableFuture存在一些问题:

  • 链中的每个任务都遵循“submit-get”模式。在长链中,这会产生冗长的代码。
  • 当链对任务失败具有容错性时,应创建专门的try-catch块。
  • 调用get()方法时,它会等待Callable返回一个值。因此,链中所有任务执行时间之和等于整个链的总执行时间。但如果下一个任务仅依赖于一个前一个任务的正确执行,那么链的处理速度会显著降低。

4. Supplier

Supplier是一个功能性接口,其SAM(单一抽象方法)是get()

它不接受任何参数,返回一个值,且只抛出未检查异常:

T get();

这个接口最常见的用例之一是延迟执行某些代码。

Optional类有几个方法接受Supplier作为参数,如Optional.or()Optional.orElseGet()等。

因此,Supplier只在Optional为空时执行。

我们也可以在异步计算上下文中使用它,特别是在CompletableFutureAPI中。

有些方法接受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;
}

使用CompletableFutureSupplier方法定义任务链可能解决之前使用FutureCallable方法时出现的一些问题:

  • 链中的每个任务都是独立的。如果任务执行失败,我们可以通过exceptionally()块进行处理。
  • join()方法不需要在编译时处理受检异常。
  • 我们可以设计一个异步任务模板,改进每个任务的状态处理。

5. 总结

在这篇文章中,我们讨论了CallableSupplier接口之间的差异,重点关注异步任务的上下文。

主要的接口设计差异在于Callable抛出的受检异常。

Callable最初并不是为函数式上下文设计的,随着时间的推移,它逐渐适应了,但函数编程和受检异常并不兼容。

因此,任何功能性的API(如CompletableFuture API)总是接受Supplier而不是Callable

如往常一样,示例的完整源代码可以在GitHub上找到。