Core Java

Guide to CompletableFuture join() vs get()

Java’s CompletableFuture is a powerful tool for asynchronous programming, offering various methods to handle and manipulate future tasks. Among these methods, join() and get() are two commonly used for waiting for the completion of a task and retrieving its result. Let us delve into understanding the key differences between CompletableFuture join vs get.

1. Overview of CompletableFuture

CompletableFuture is a part of Java’s java.util.concurrent package, introduced in Java 8. A CompletableFuture is a versatile tool in Java’s concurrency API that allows you to write non-blocking, asynchronous code. It provides methods to create, combine, and flexibly execute asynchronous tasks. By using CompletableFuture, you can handle long-running computations without blocking the main thread, thus improving the performance and responsiveness of your application.

1.1 The join() Method

The join() method is a part of the CompletableFuture class and is used to wait for the completion of the asynchronous task. Unlike get(), join() does not throw a checked exception.

public T join()

The method waits if necessary for the computation to complete and then retrieves its result. If the computation completes exceptionally, join() wraps the exception in an UncheckedExecutionException and rethrows it.

1.2 The get() Method

The get() method, also part of the CompletableFuture class, is used to retrieve the result of the computation, blocking if necessary until the computation is complete.

public T get() throws InterruptedException, ExecutionException

This method waits if necessary for the computation to complete and then retrieves its result. If the computation completes exceptionally, get() throws the checked exceptions InterruptedException or ExecutionException.

2. Comparison: join() vs. get()

While both join() and get() serve to wait for a CompletableFuture to complete and retrieve its result, there are key differences:

  • Exception Handling: join() throws an unchecked exception (CompletionException), while get() throws checked exceptions (InterruptedException and ExecutionException).
  • Usage Scenario: Use join() when you want to avoid handling checked exceptions, typically in a lambda or method reference context. Use get() when you need to handle interruptions and other execution exceptions explicitly.
  • Blocking Behavior: Both methods block the calling thread until the computation is complete.

3. Code Example and Breakdown

Here is an example demonstrating the use of join() and get():

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

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
            return "Hello, World!";
        });

        // Using join()
        String resultJoin = future.join();
        System.out.println("Result using join: " + resultJoin);

        // Using get()
        try {
            String resultGet = future.get();
            System.out.println("Result using get: " + resultGet);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

In this example, a CompletableFuture is created to run a task asynchronously. The task simulates a delay using Thread.sleep(). The result is then retrieved using both join() and get() methods, demonstrating their usage and behavior.

3.1 Code Output and Explanation

Here is the output of the provided code example:

Result using join: Hello, World!
Result using get: Hello, World!

3.1.1 Explanation

  • Asynchronous Task:
    • The code creates a CompletableFuture using supplyAsync(), which runs a task asynchronously.
    • This task simulates a delay of 2 seconds using Thread.sleep(2000), then returns the string “Hello, World!”.
  • Using join():
    • future.join() is called, which waits for the asynchronous task to complete.
    • Once the task is complete, join() returns the result, “Hello, World!”, and prints it.
  • Using get():
    • future.get() is called, which also waits for the asynchronous task to complete.
    • Similar to join(), get() returns the result, “Hello, World!”, and prints it.
    • If the task were interrupted or failed, get() would throw InterruptedException or ExecutionException, respectively. However, in this case, the task was completed successfully.

3.2 Output When an Exception Occurs

Now let’s say if we modify the code to throw the exception within the asynchronous task, both join() and get() will handle it differently:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Task failed");
});

3.2.1 Output Using join()

The join() method will throw an unchecked CompletionException, which wraps up the original exception.

Exception in thread "main" java.util.concurrent.CompletionException: java.lang.RuntimeException: Task failed
    at java.base/java.util.concurrent.CompletableFuture.join(CompletableFuture.java:2046)
    at CompletableFutureExample.main(CompletableFutureExample.java:10)
Caused by: java.lang.RuntimeException: Task failed
    at CompletableFutureExample.lambda$main$0(CompletableFutureExample.java:7)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    ... 1 more

3.2.2 Output Using get()

The get() method will throw ExecutionException, which wraps up the original exception.

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: Task failed
    at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:389)
    at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2001)
    at CompletableFutureExample.main(CompletableFutureExample.java:15)
Caused by: java.lang.RuntimeException: Task failed
    at CompletableFutureExample.lambda$main$0(CompletableFutureExample.java:7)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    ... 1 more

4. Conclusion

Both join() and get() methods of CompletableFuture provide mechanisms to wait for the completion of an asynchronous task and retrieve its result. The choice between them depends on your specific use case and how you prefer to handle exceptions. Understanding these differences helps you write more robust and maintainable asynchronous code in Java.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button