Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CompletableFuture

The Future from the previous lesson can do only one thing — wait for the result with get(). CompletableFuture goes further: you can attach the next steps to a future result in advance — "when it's loaded, transform it", "when both are ready, combine them". Such code is called asynchronous: tasks run in the background, and the main thread doesn't sit idle waiting for each of them.

The first asynchronous task

CompletableFuture.supplyAsync runs a task on a background thread and immediately returns a promise object. The join() method waits for the result:

void main() {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 6 * 7);
 
    IO.println("Result: " + future.join());
}

Output:

Result: 42

join() is like get() from Future, but it doesn't require handling checked exceptions. By default tasks run in the shared thread pool ForkJoinPool.commonPool().

A chain of steps

thenApply transforms the result once it's ready — like map on streams. thenAccept receives the final value and returns nothing:

void pause(long millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}
 
String loadName(int userId) {
    pause(200);
    return "anna";
}
 
void main() {
    CompletableFuture<Void> done = CompletableFuture
            .supplyAsync(() -> loadName(7))
            .thenApply(name -> name.substring(0, 1).toUpperCase() + name.substring(1))
            .thenApply(name -> "Hello, " + name + "!")
            .thenAccept(text -> IO.println(text));
 
    IO.println("Waiting for the load...");
    done.join();
}

Output:

Waiting for the load...
Hello, Anna!

"Waiting for the load..." came first: the main thread didn't wait for the background task to load the name. The pause method hides the checked InterruptedException from Thread.sleep.

Combining two tasks

thenCombine waits for two independent tasks and combines their results. The tasks run in parallel:

void pause(long millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}
 
void main() {
    CompletableFuture<Double> price = CompletableFuture.supplyAsync(() -> {
        pause(300);
        return 20.0;
    });
    CompletableFuture<Double> rate = CompletableFuture.supplyAsync(() -> {
        pause(300);
        return 0.9;
    });
 
    double total = price.thenCombine(rate, (p, r) -> p * r).join();
    IO.println("Price in euros: " + total);
}

Output:

Price in euros: 18.0

Both 300 ms tasks ran at the same time, so the total is ready after about 300 ms, not 600.

Many tasks

CompletableFuture.allOf returns a promise that completes when all the given tasks finish. After that the results can be collected with join() in the order you need:

void main() {
    List<String> cities = List.of("Rome", "Oslo", "Lima");
 
    List<CompletableFuture<String>> futures = cities.stream()
            .map(city -> CompletableFuture.supplyAsync(() -> city + ": " + city.length() * 5 + "°"))
            .toList();
 
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
 
    for (CompletableFuture<String> f : futures) {
        IO.println(f.join());
    }
}

Output:

Rome: 20°
Oslo: 20°
Lima: 20°

Handling errors

If a task in the chain throws an exception, the following thenApply steps are skipped, and exceptionally lets you substitute a fallback value:

void main() {
    String result = CompletableFuture
            .supplyAsync(() -> Integer.parseInt("hundred"))
            .thenApply(n -> "Number: " + n)
            .exceptionally(e -> "Error: " + e.getCause().getMessage())
            .join();
 
    IO.println(result);
}

Output:

Error: For input string: "hundred"

The exception reaches exceptionally wrapped in a CompletionException, so the original error is obtained with getCause().

Common mistake

If you don't wait for the result, the program may end before the background task. Threads of the shared pool are daemon threads: the JVM doesn't wait for them when main exits:

void pause(long millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}
 
void main() {
    CompletableFuture.runAsync(() -> {
        pause(500);
        IO.println("Report saved");
    });
    IO.println("Program finished");
}

Output:

Program finished

The "Report saved" line never appeared. Keep the promise in a variable and call join() on it before exiting.

Thanks for reading!

Donate to the site — we work hard for you.