CompletableFuture in Java

CompletableFuture in Java

In today’s fast-paced software development world, asynchronous programming is essential for building efficient and responsive applications. Java provides a powerful tool for managing asynchronous tasks through the CompletableFuture class. In this blog post, we’ll explore what asynchronous programming is, how CompletableFuture in java fits into this paradigm, and how you can leverage it to write cleaner and more performant code.

Understanding Asynchronous Programming

Before diving into CompletableFuture, it’s important to understand the concept of asynchronous programming.

Asynchronous Programming is a programming paradigm that allows a program to perform tasks in the background without blocking the main thread. This is particularly useful in scenarios where you have tasks that involve waiting, such as:

  • I/O operations: Reading from or writing to files, network communications, etc.
  • Long computations: Tasks that take a significant amount of time to complete.
  • User interactions: Operations that should not freeze the user interface, such as responding to clicks or input.

In traditional synchronous programming, if a task takes time to complete, it blocks the execution of subsequent tasks. For example, if you have a method that reads data from a file, the program must wait until the file reading is complete before it can continue executing the next line of code. This can lead to inefficient use of resources and a poor user experience.

Asynchronous programming allows your program to continue executing while the time-consuming task is being processed. This is achieved using constructs such as callbacks, promises, and futures, which enable your program to handle multiple operations concurrently.

What is CompletableFuture in Java 8?

Introduced in Java 8, CompletableFuture is part of the java.util.concurrent package. It represents a future result of an asynchronous computation. Unlike the traditional Future interface, CompletableFuture provides a more flexible and comprehensive API for handling asynchronous programming.

Key Features of CompletableFuture

  • Non-blocking Operations: CompletableFuture allows you to execute tasks asynchronously without blocking the main thread.
  • Pipeline Support: It supports chaining multiple asynchronous tasks, making it easy to handle complex workflows.
  • Exception Handling: It provides robust methods for handling exceptions that might occur during asynchronous execution.
  • Combine Futures: You can combine multiple futures to achieve more complex asynchronous workflows.

Basic Usage of CompletableFuture

Let’s start with a basic example to understand how CompletableFuture works. Suppose you want to perform a simple asynchronous computation of adding two numbers.

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // Simulating a delay
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 5 + 10;
        });

        future.thenAccept(result -> System.out.println("The result is: " + result));
    }
}

In this example:

  1. CompletableFuture.supplyAsync starts an asynchronous computation that adds two numbers.
  2. thenAccept is a callback that is executed when the computation completes, printing the result.

Chaining Asynchronous Tasks

One of the powerful features of CompletableFuture is the ability to chain multiple asynchronous tasks. Let’s enhance the previous example to include a second computation that multiplies the result.

import java.util.concurrent.CompletableFuture;

public class ChainingExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // Simulating a delay
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 5 + 10;
        });

        CompletableFuture<Integer> chainedFuture = future.thenApply(result -> {
            // Chaining another computation
            return result * 2;
        });

        chainedFuture.thenAccept(result -> System.out.println("The final result is: " + result));
    }
}

Here’s what happens in this example:

  1. supplyAsync performs the initial addition.
  2. thenApply is used to multiply the result by 2.
  3. thenAccept prints the final result.

Combining Multiple Futures

Combining multiple futures is another powerful feature of CompletableFuture. Imagine you need to fetch user data and then fetch related posts concurrently. You can combine these futures as follows:

import java.util.concurrent.CompletableFuture;

public class CombiningFuturesExample {
    public static void main(String[] args) {
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            // Simulating user data fetching
            return "User data";
        });

        CompletableFuture<String> postsFuture = CompletableFuture.supplyAsync(() -> {
            // Simulating posts fetching
            return "Posts data";
        });

        CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(userFuture, postsFuture);

        combinedFuture.thenRun(() -> {
            try {
                // Retrieve results from the futures
                String userData = userFuture.get();
                String postsData = postsFuture.get();

                System.out.println("User Data: " + userData);
                System.out.println("Posts Data: " + postsData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

In this example:

  1. Two CompletableFuture instances are created for fetching user data and posts.
  2. CompletableFuture.allOf combines these futures and ensures that both complete before proceeding.
  3. thenRun retrieves and prints the results once both futures have completed.

Handling Exceptions

Proper exception handling is crucial in asynchronous programming. CompletableFuture provides methods to handle exceptions effectively. Here’s an example:

import java.util.concurrent.CompletableFuture;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // Simulating an error
            if (true) {
                throw new RuntimeException("Something went wrong");
            }
            return 10;
        });

        future.handle((result, ex) -> {
            if (ex != null) {
                System.out.println("Exception occurred: " + ex.getMessage());
                return 0; // Default value in case of an error
            }
            return result;
        }).thenAccept(result -> System.out.println("Result is: " + result));
    }
}

In this example:

  1. handle is used to process both the result and any exception that may have occurred.
  2. If an exception is thrown, it is handled gracefully, and a default value is returned.

Conclusion

CompletableFuture is a versatile tool for handling asynchronous programming in Java. By understanding its core features and capabilities, you can write cleaner, more efficient code that handles asynchronous tasks with ease. Whether you’re chaining tasks, combining multiple futures, or handling exceptions, CompletableFuture provides the flexibility you need to build robust and responsive applications.

Asynchronous programming might seem complex at first, but with tools like CompletableFuture, you can manage concurrency effectively and enhance your application’s performance and responsiveness.

Happy coding!

Method Reference in Java 8

Method Reference in Java 8 allows a functional interface method to be mapped to a specific method using the :: (double colon) operator. This technique simplifies the implementation of functional interfaces by directly referencing existing methods. The referenced method can be either a static method or an instance method. It’s important that the functional interface method and the specified method have matching argument types, while other elements such as return type, method name, and modifiers can differ.

If the specified method is a static method, the syntax is:

ClassName::methodName

If the method is an instance method, the syntax is:

ObjectReference::methodName

A functional interface can refer to a lambda expression and can also refer to a method reference. Therefore, a lambda expression can be replaced with a method reference, making method references an alternative syntax to lambda expressions.

Example with Lambda Expression

class Task {
    public static void main(String[] args) {
        Runnable r = () -> {
            for (int i = 0; i <= 10; i++) {
                System.out.println("Child Thread");
            }
        };
        Thread t = new Thread(r);
        t.start();

        for (int i = 0; i <= 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Example with Method Reference

class Task {
    public static void printChildThread() {
        for (int i = 0; i <= 10; i++) {
            System.out.println("Child Thread");
        }
    }

    public static void main(String[] args) {
        Runnable r = Task::printChildThread;
        Thread t = new Thread(r);
        t.start();

        for (int i = 0; i <= 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

In the above example, the Runnable interface’s run() method is referring to the Task class’s static method printChildThread().

Method Reference to an Instance Method

interface Processor {
    void process(int i);
}

class Worker {
    public void display(int i) {
        System.out.println("From Method Reference: " + i);
    }

    public static void main(String[] args) {
        Processor p = i -> System.out.println("From Lambda Expression: " + i);
        p.process(10);

        Worker worker = new Worker();
        Processor p1 = worker::display;
        p1.process(20);
    }
}

In this example, the functional interface method process() is referring to the Worker class instance method display().

The main advantage of method references is that we can reuse existing code to implement functional interfaces, enhancing code reusability.

Constructor Reference in Java 8

We can use the :: (double colon) operator to refer to constructors as well.

Syntax:

ClassName::new
Example:
class Product {
    private String name;

    Product(String name) {
        this.name = name;
        System.out.println("Constructor Executed: " + name);
    }
}

interface Creator {
    Product create(String name);
}

class Factory {
    public static void main(String[] args) {
        Creator c = name -> new Product(name);
        c.create("From Lambda Expression");

        Creator c1 = Product::new;
        c1.create("From Constructor Reference");
    }
}

In this example, the functional interface Creator is referring to the Product class constructor.

Note: In method and constructor references, the argument types must match.

Here is the link for the Java 8 quiz:
Click here

Related Articles:

Java 8 Functional Interfaces: Features and Benefits

Java 8 Functional Interfaces: Features and Benefits

Java 8 functional interfaces, which are interfaces containing only one abstract method. The method itself is known as the functional method or Single Abstract Method (SAM). Examples include:

Predicate: Represents a predicate (boolean-valued function) of one argument. Contains only the test() method, which evaluates the predicate on the given argument.

Supplier: Represents a supplier of results. Contains only the get() method, which returns a result.

Consumer: Represents an operation that accepts a single input argument and returns no result. Contains only the accept() method, which performs the operation on the given argument.

Function: Represents a function that accepts one argument and produces a result. Contains only the apply() method, which applies the function to the given argument.

BiFunction: Represents a function that accepts two arguments and produces a result. Contains only the apply() method, which applies the function to the given arguments.

Runnable: Represents a task that can be executed. Contains only the run() method, which is where the task logic is defined.

Comparable: Represents objects that can be ordered. Contains only the compareTo() method, which compares this object with the specified object for order.

ActionListener: Represents an action event listener. Contains only the actionPerformed() method, which is invoked when an action occurs.

Callable: Represents a task that returns a result and may throw an exception. Contains only the call() method, which executes the task and returns the result.

Java 8 Functional Interfaces

Benefits of @FunctionalInterface Annotation

The @FunctionalInterface annotation was introduced to explicitly mark an interface as a functional interface. It ensures that the interface has only one abstract method and allows additional default and static methods.

In a functional interface, besides the single abstract method (SAM), any number of default and static methods can also be defined. For instance:

interface ExampleInterface {
    void method1(); // Abstract method

    default void method2() {
        System.out.println("Hello"); // Default method
    }
}

Java 8 introduced the @FunctionalInterface annotation to explicitly mark an interface as a functional interface:

@FunctionalInterface
interface ExampleInterface {
    void method1();
}

It’s important to note that a functional interface can have only one abstract method. If there are more than one abstract methods, a compilation error occurs.

Functional Interface in java

Inheritance in Functional Interfaces

If an interface extends a functional interface and does not contain any abstract methods itself, it remains a functional interface. For example:

@FunctionalInterface
interface A {
    void methodOne();
}

@FunctionalInterface
interface B extends A {
    // Valid to extend and not add more abstract methods
}

However, if the child interface introduces any new abstract methods, it ceases to be a functional interface and using @FunctionalInterface will result in a compilation error.

Lambda Expressions and Functional Interfaces:

Lambda expressions are used to invoke the functionality defined in functional interfaces. They provide a concise way to implement functional interfaces. For example:

Without Lambda Expression:

interface ExampleInterface {
    void methodOne();
}

class Demo implements ExampleInterface {
    public void methodOne() {
        System.out.println("Method one execution");
    }

    public class Test {
        public static void main(String[] args) {
            ExampleInterface obj = new Demo();
            obj.methodOne();
        }
    }
}

With Lambda Expression:

interface ExampleInterface {
    void methodOne();
}

class Test {
    public static void main(String[] args) {
        ExampleInterface obj = () -> System.out.println("Method one execution");
        obj.methodOne();
    }
}

Advantages of Lambda Expressions:

  1. They reduce code length, improving readability.
  2. They simplify complex implementations of anonymous inner classes.
  3. They can be used wherever functional interfaces are applicable.

Anonymous Inner Classes vs Lambda Expressions:

Lambda expressions are often used to replace anonymous inner classes, reducing code length and complexity. For example:

With Anonymous Inner Class:

class Test {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        });
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

With Lambda Expression:

class Test {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Child Thread");
            }
        });
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Differences between Anonymous Inner Classes and Lambda Expressions

Anonymous Inner ClassLambda Expression
A class without a nameA method without a name (anonymous function)
Can extend concrete and abstract classesCannot extend concrete or abstract classes
Can implement interfaces with any number of methodsCan only implement interfaces with a single abstract method
Can declare instance variablesCannot declare instance variables; variables are treated as final
Has separate .class file generated at compilationNo separate .class file; converts into a private method
In summary, lambda expressions offer a concise and effective way to implement functional interfaces, enhancing code readability and reducing complexity compared to traditional anonymous inner classes.
Click here

Related Articles: