Java CompletableFuture Tutorial

Introduction

CompletableFuture is a class in the java.util.concurrent package that provides a way to write asynchronous, non-blocking code in Java. It is part of the Java 8 enhancements to the Future interface, which previously provided a way to handle long-running tasks but lacked support for chaining and combining results of multiple asynchronous computations.

Table of Contents

  1. What is CompletableFuture?
  2. Creating a CompletableFuture
  3. Running Tasks Asynchronously
  4. Chaining Tasks
  5. Combining Multiple CompletableFutures
  6. Handling Errors
  7. Waiting for Completion
  8. Example Programs
  9. Conclusion

1. What is CompletableFuture?

CompletableFuture is an implementation of the Future interface that allows you to write asynchronous code. It provides methods for starting, chaining, and combining asynchronous tasks. It can also handle exceptions gracefully, making it used for concurrent programming in Java.

2. Creating a CompletableFuture

You can create a CompletableFuture instance in several ways:

Example:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        // Creating a CompletableFuture instance
        CompletableFuture<String> future = new CompletableFuture<>();

        // Completing the CompletableFuture manually
        future.complete("Hello, World!");

        // Getting the result
        try {
            System.out.println(future.get()); // Output: Hello, World!
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Output:

Hello, World!

3. Running Tasks Asynchronously

You can run tasks asynchronously using methods like runAsync and supplyAsync.

Example:

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

public class CompletableFutureAsyncExample {
    public static void main(String[] args) {
        // Running a task asynchronously
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("Task completed!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Waiting for the task to complete
        try {
            future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Task completed!

4. Chaining Tasks

You can chain tasks using methods like thenApply, thenAccept, and thenRun.

Example:

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

public class CompletableFutureChainingExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Hello";
        }).thenApply(result -> {
            return result + ", World!";
        });

        try {
            System.out.println(future.get()); // Output: Hello, World!
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Hello, World!

5. Combining Multiple CompletableFutures

You can combine multiple CompletableFutures using methods like thenCombine, thenCompose, and allOf.

Example:

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

public class CompletableFutureCombiningExample {
    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            return "Hello";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            return "World";
        });

        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + " " + result2;
        });

        try {
            System.out.println(combinedFuture.get()); // Output: Hello World
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Hello World

6. Handling Errors

You can handle errors in CompletableFuture using methods like exceptionally and handle.

Example:

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

public class CompletableFutureErrorHandlingExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) {
                throw new RuntimeException("Something went wrong!");
            }
            return "Hello, World!";
        }).exceptionally(ex -> {
            return "Error: " + ex.getMessage();
        });

        try {
            System.out.println(future.get()); // Output: Error: Something went wrong!
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Error: Something went wrong!

7. Waiting for Completion

You can wait for the completion of CompletableFuture using methods like join, get, and getNow.

Example:

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

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

        // Waiting for the future to complete
        try {
            System.out.println(future.get()); // Output: Hello, World!
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Hello, World!

8. Example Programs

Example 1: Sequential Execution of Asynchronous Tasks

Example:

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

public class CompletableFutureSequentialExample {
    public static void main(String[] args) {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("Task 1");
        }).thenRunAsync(() -> {
            System.out.println("Task 2");
        }).thenRunAsync(() -> {
            System.out.println("Task 3");
        });

        // Waiting for all tasks to complete
        try {
            future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Task 1
Task 2
Task 3

Example 2: Combining Results of Multiple Futures

Example:

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

public class CompletableFutureCombineExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            return 10;
        });

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            return 20;
        });

        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        });

        try {
            System.out.println("Combined Result: " + combinedFuture.get()); // Output: Combined Result: 30
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Output:

Combined Result: 30

9. Conclusion

CompletableFuture in Java provides a powerful and flexible way to handle asynchronous programming. It allows you to run tasks asynchronously, chain tasks, combine multiple futures, and handle errors gracefully. By understanding and using CompletableFuture, you can write more efficient and responsive applications.

Happy coding!

Comments