MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 中 CompletableFuture 任务异步回调 thenApply 方法

2023-10-063.5k 阅读

一、CompletableFuture 简介

在 Java 8 之前,处理异步任务相对繁琐,通常需要借助 Future 接口来实现,但 Future 存在一些局限性,比如获取结果时需要阻塞等待,无法方便地进行链式调用以及处理异步任务的组合等。Java 8 引入了 CompletableFuture,它实现了 Future 接口和 CompletionStage 接口,为异步编程带来了极大的便利。

CompletableFuture 可以手动完成任务,也可以在任务完成时触发回调,并且支持链式调用和任务组合等强大功能。它让我们能够以更简洁、更高效的方式编写异步代码,提升程序的性能和响应性。

二、thenApply 方法概述

thenApply 方法是 CompletableFuture 提供的一个用于异步任务回调的重要方法。它的作用是在当前 CompletableFuture 任务完成后,对其结果进行转换,并返回一个新的 CompletableFuture

其方法签名如下:

<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);

其中,T 是当前 CompletableFuture 的结果类型,U 是经过 fn 函数转换后新的 CompletableFuture 的结果类型。fn 是一个 Function 接口的实现,用于对当前 CompletableFuture 的结果进行转换操作。

三、thenApply 方法原理深入剖析

  1. 任务完成触发回调CompletableFuture 代表的异步任务完成时(无论是正常完成还是异常完成),会触发 thenApply 注册的回调函数。这里的任务完成,可能是通过 CompletableFuturecomplete 方法手动完成,也可能是异步任务执行结束返回结果。

  2. 结果转换逻辑 thenApply 中的 Function 函数接收当前 CompletableFuture 的结果作为参数,并返回一个新的结果。这个新的结果将作为新的 CompletableFuture 的结果。例如,如果当前 CompletableFuture 代表的任务返回一个 Integer 类型的数字,我们可以通过 thenApply 中的 Function 将其转换为 String 类型。

  3. 新 CompletableFuture 的创建与状态管理 thenApply 方法返回一个新的 CompletableFuture。这个新的 CompletableFuture 的状态与当前 CompletableFuture 的完成情况紧密相关。如果当前 CompletableFuture 正常完成,新的 CompletableFuture 会通过调用 Function 函数对结果进行转换,并将转换后的结果设置为自身的结果,从而也进入完成状态。如果当前 CompletableFuture 异常完成,新的 CompletableFuture 也会以相同的异常进入异常完成状态,而不会调用 Function 函数。

四、thenApply 方法的代码示例

  1. 简单的数值转换示例
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ThenApplyExample1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            // 模拟一个异步任务,这里返回 5
            return 5;
        });

        CompletableFuture<String> future2 = future1.thenApply(result -> {
            // 将 Integer 类型的结果转换为 String 类型
            return "结果是:" + result;
        });

        System.out.println(future2.get());
    }
}

在这个示例中,CompletableFuture.supplyAsync 方法创建了一个异步任务,该任务返回一个 Integer 类型的结果 5。然后通过 thenApply 方法,将这个 Integer 类型的结果转换为 String 类型,并返回一个新的 CompletableFuture。最后通过 get 方法获取最终的结果并打印。

  1. 结合复杂业务逻辑的示例 假设我们有一个电商系统,需要查询商品价格并根据价格计算折扣后的价格。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ThenApplyExample2 {
    // 模拟查询商品价格的异步方法
    public static CompletableFuture<Double> getProductPrice(String productId) {
        return CompletableFuture.supplyAsync(() -> {
            // 这里简单返回一个固定价格,实际应用中会从数据库或其他数据源查询
            return 100.0;
        });
    }

    // 模拟计算折扣价格的方法
    public static double calculateDiscountedPrice(double price) {
        // 简单的折扣计算,打 8 折
        return price * 0.8;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Double> priceFuture = getProductPrice("12345");

        CompletableFuture<Double> discountedPriceFuture = priceFuture.thenApply(ThenApplyExample2::calculateDiscountedPrice);

        System.out.println("折扣后的价格是:" + discountedPriceFuture.get());
    }
}

在这个示例中,getProductPrice 方法模拟了一个异步查询商品价格的操作,返回一个 CompletableFuture<Double>。然后通过 thenApply 方法,将获取到的商品价格传递给 calculateDiscountedPrice 方法进行折扣计算,得到折扣后的价格,并返回一个新的 CompletableFuture<Double>

  1. 处理异常情况的示例
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ThenApplyExample3 {
    public static CompletableFuture<Integer> divideNumbers(int a, int b) {
        return CompletableFuture.supplyAsync(() -> {
            if (b == 0) {
                throw new IllegalArgumentException("除数不能为 0");
            }
            return a / b;
        });
    }

    public static void main(String[] args) {
        CompletableFuture<Integer> future = divideNumbers(10, 2);

        CompletableFuture<String> resultFuture = future.thenApply(result -> {
            return "结果是:" + result;
        });

        resultFuture.thenAccept(System.out::println)
               .exceptionally(ex -> {
                    System.out.println("发生异常:" + ex.getMessage());
                    return null;
                });

        // 这里也可以直接在 future 上处理异常
        future.exceptionally(ex -> {
            System.out.println("除法发生异常:" + ex.getMessage());
            return -1;
        }).thenApply(result -> {
            if (result == -1) {
                return "计算失败";
            }
            return "结果是:" + result;
        }).thenAccept(System.out::println);
    }
}

在这个示例中,divideNumbers 方法模拟了一个可能抛出异常的异步除法操作。如果除法操作正常完成,thenApply 方法会将结果转换为字符串。如果发生异常,通过 exceptionally 方法可以捕获并处理异常。这里展示了两种处理异常的方式,一种是在 thenApply 后的 CompletableFuture 上处理,另一种是在原始的 CompletableFuture 上处理。

五、thenApply 方法与其他 CompletableFuture 方法的组合使用

  1. 与 thenCompose 方法组合 thenCompose 方法也是 CompletableFuture 提供的用于链式调用的方法,但它与 thenApply 有一些区别。thenApply 返回的是一个包含转换后结果的新 CompletableFuture,而 thenCompose 返回的是 Function 函数返回的 CompletableFuture 本身。

例如,假设我们有两个异步任务,第一个任务返回一个用户 ID,第二个任务需要根据这个用户 ID 查询用户详细信息:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ThenApplyAndThenComposeExample {
    // 模拟根据用户 ID 查询用户详细信息的异步方法
    public static CompletableFuture<String> getUserDetailsById(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            // 这里简单返回一个固定的用户信息,实际应用中会从数据库查询
            return "用户" + userId + "的详细信息";
        });
    }

    public static CompletableFuture<Integer> getUserId() {
        return CompletableFuture.supplyAsync(() -> {
            // 这里简单返回一个固定的用户 ID
            return 1;
        });
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future1 = getUserId().thenApply(id -> {
            // 这里如果使用 thenApply,返回的是 CompletableFuture<CompletableFuture<String>>
            return getUserDetailsById(id);
        });

        CompletableFuture<String> future2 = getUserId().thenCompose(ThenApplyAndThenComposeExample::getUserDetailsById);

        System.out.println(future1.get().get());
        System.out.println(future2.get());
    }
}

在这个示例中,thenApply 方法返回的是一个 CompletableFuture<CompletableFuture<String>>,需要嵌套获取结果。而 thenCompose 方法直接返回 getUserDetailsById 方法返回的 CompletableFuture<String>,更方便地获取最终结果。

  1. 与 thenAccept 方法组合 thenAccept 方法用于在 CompletableFuture 任务完成后执行一个消费操作,但不返回新的结果。我们可以将 thenApplythenAccept 组合使用,先对结果进行转换,然后进行消费。
import java.util.concurrent.CompletableFuture;

public class ThenApplyAndThenAcceptExample {
    public static CompletableFuture<Integer> calculateResult() {
        return CompletableFuture.supplyAsync(() -> {
            return 10;
        });
    }

    public static void main(String[] args) {
        calculateResult().thenApply(result -> result * 2)
               .thenAccept(finalResult -> System.out.println("最终结果是:" + finalResult));
    }
}

在这个示例中,calculateResult 方法返回一个 CompletableFuture<Integer>。通过 thenApply 方法将结果乘以 2 进行转换,然后通过 thenAccept 方法打印最终结果。

六、使用 thenApply 方法的注意事项

  1. 线程安全性 虽然 CompletableFuture 本身是线程安全的,但在 thenApply 中的 Function 函数如果涉及到共享资源的访问和修改,需要注意线程安全问题。例如,如果 Function 函数中访问了一个共享的可变对象,多个线程同时执行这个 Function 可能会导致数据竞争和不一致的问题。

  2. 异常处理thenApply 中,如果 Function 函数抛出异常,会导致返回的新 CompletableFuture 进入异常完成状态。需要通过 exceptionally 等方法来妥善处理这些异常,避免程序出现未处理的异常导致崩溃。

  3. 性能问题 虽然 CompletableFuture 提供了强大的异步编程能力,但如果在 thenApply 中执行的 Function 函数包含大量的计算或阻塞操作,可能会影响程序的整体性能。尽量保持 Function 函数的简洁和高效,对于复杂的计算可以考虑进一步异步化。

  4. 内存泄漏风险 如果在 thenApply 中创建了大量的对象,并且这些对象没有及时释放,可能会导致内存泄漏问题。尤其是在高并发的场景下,需要仔细检查资源的使用和释放情况。

七、实际应用场景

  1. 电商系统中的订单处理 在电商系统中,当用户下单后,可能需要进行一系列异步操作,如库存检查、价格计算、订单记录等。可以使用 CompletableFuturethenApply 方法来对每个步骤的结果进行转换和处理。例如,在库存检查通过后,通过 thenApply 计算订单总价,并继续后续的订单记录操作。

  2. 大数据处理 在大数据处理场景中,可能需要对从数据源获取的数据进行一系列的转换和处理。可以使用 CompletableFuture 异步获取数据,然后通过 thenApply 方法对数据进行清洗、转换等操作,提高数据处理的效率。

  3. 分布式系统中的服务调用 在分布式系统中,微服务之间的调用通常是异步的。当一个微服务调用另一个微服务获取结果后,可能需要对结果进行处理并调用下一个微服务。CompletableFuturethenApply 方法可以方便地实现这种链式调用和结果转换,使得分布式系统的服务编排更加灵活和高效。

八、总结 thenApply 方法的优势

  1. 简洁的链式调用 通过 thenApply 方法,我们可以方便地对异步任务的结果进行链式处理,使得代码结构更加清晰,易于理解和维护。相比传统的异步编程方式,不需要手动管理任务的依赖关系和结果传递,大大减少了代码的复杂性。

  2. 灵活的结果转换 thenApply 允许我们根据实际需求对异步任务的结果进行任意转换,无论是简单的类型转换还是复杂的业务逻辑处理,都可以通过实现 Function 接口来完成。这种灵活性使得 CompletableFuture 能够适应各种不同的应用场景。

  3. 异步任务组合与扩展 结合 CompletableFuture 的其他方法,如 thenComposethenAccept 等,thenApply 方法可以实现强大的异步任务组合和扩展功能。我们可以轻松地构建复杂的异步任务链,满足不同业务场景下的需求。

  4. 提升程序性能和响应性 由于 CompletableFuture 是基于异步和并行计算的,使用 thenApply 方法可以充分利用多核处理器的性能,避免阻塞主线程,从而提升程序的整体性能和响应性。在高并发和对响应时间要求较高的应用场景中,这一优势尤为明显。

通过深入理解 CompletableFuturethenApply 方法,并合理地应用在实际项目中,我们能够编写出更加高效、灵活和健壮的异步程序。无论是简单的数值转换,还是复杂的分布式系统服务编排,thenApply 都能为我们提供强大的支持。在使用过程中,注意处理好线程安全、异常处理、性能优化等问题,充分发挥其优势,为我们的程序带来更好的用户体验和更高的运行效率。同时,不断探索 thenApply 与其他 CompletableFuture 方法的组合使用,能够进一步挖掘异步编程的潜力,满足日益复杂的业务需求。在实际应用场景中,如电商系统、大数据处理和分布式系统等,thenApply 方法都有着广泛的应用前景,帮助我们构建出更加优秀的软件系统。