Learn how to use synchronous and asynchronous callbacks in Java—including callbacks with lambda expressions, CompletableFuture, and more.
A callback operation in Java is one function that is passed to another function and executed after some action is completed. A callback can be executed either synchronously or asynchronously. In the case of a synchronous callback, one function is executed right after another. In the case of an asynchronous callback, a function is executed after an undetermined period of time and happens in no particular sequence with other functions.
This article introduces you to callbacks in Java, starting with the classic example of the callback as a listener in the Observable design pattern. You will see examples of a variety of synchronous and asynchronous callback implementations, including a functional callback using CompletableFuture
.
Synchronous callbacks in Java
A synchronous callback function will be always executed right after some action is performed. That means that it will be synchronized with the function performing the action.
As I mentioned, an example of a callback function is found in the Observable design pattern. In a UI that requires a button click to initiate some action, we can pass the callback function as a listener on that button click. The listener function waits until the button is clicked, then executes the listener callback.
Now let’s look at a few examples of the callback concept in code.
Anonymous inner class callback
Anytime we pass an interface with a method implementation to another method in Java, we are using the concept of a callback function. In the following code, we will pass the Consumer
functional interface and an anonymous inner class (implementation without a name) to implement the accept()
method.
Once the accept()
method is implemented, we’ll execute the action from the performAction
method; then we’ll execute the accept()
method from the Consumer
interface:
import java.util.function.Consumer;
public class AnonymousClassCallback {
public static void main(String[] args) {
performAction(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
}
public static void performAction(Consumer<String> consumer) {
System.out.println("Action is being performed...");
consumer.accept("Callback is executed");
}
}
The output from this code is the print statement:
Action is being performed...
Callback is executed...
In this code, we passed the Consumer
interface to the performAction()
method, then invoked the accept()
method after the action was finished.
You might also notice that using an anonymous inner class is quite verbose. It would be much better to use a lambda instead. Let’s see what happens when we use the lambda for our callback function.
Lambda callback
In Java, we can implement the functional interface with a lambda expression and pass it to a method, then execute the function after an operation is finished. Here’s how that looks in code:
public class LambdaCallback {
public static void main(String[] args) {
performAction(() -> System.out.println("Callback function executed..."));
}
public static void performAction(Runnable runnable) {
System.out.println("Action is being performed...");
runnable.run();
}
}
Once again, the output states that the action is being performed and the callback executed.
In this example, you might notice that we passed the Runnable
functional interface in the performAction
method. Therefore, we were able to override and execute the run()
method after the action from the performAction
method was finished.
Asynchronous callbacks
Often, we want to use an asynchronous callback method, which means a method that will be invoked after the action but asynchronously with other processes. That might help in performance when the callback method does not need to be invoked immediately following the other process.
Simple thread callback
Let’s start with the simplest way we can make this asynchronous callback call operation. In the following code, first we will implement the run()
method from a Runnable
functional interface. Then, we will create a Thread
and use the run()
method we’ve just implemented within the Thread
. Finally, we will start the Thread
to execute asynchronously:
public class AsynchronousCallback {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Callback executed...");
AsynchronousCallback asynchronousCallback = new AsynchronousCallback();
asynchronousCallback.performAsynchronousAction(runnable);
}
public void performAsynchronousAction(Runnable runnable) {
new Thread(() -> {
System.out.println("Processing Asynchronous Task...");
runnable.run();
}).start();
}
}
The output in this case is:
Processing Asynchronous Task...
Callback executed...
Notice in the code above that first we created an implementation for the run()
method from Runnable
. Then, we invoked the performAsynchronousAction()
method, passing the runnable
functional interface with the run()
method implementation.
Within the performAsynchronousAction()
we pass the runnable
interface and implement the other Runnable
interface inside the Thread
with a lambda. Then we print “Processing Asynchronous Task…” Finally, we invoke the callback function run
that we passed by parameter, printing “Callback executed…”
Asynchronous parallel callback
Other than invoking the callback function within the asynchronous operation, we could also invoke a callback function in parallel with another function. This means that we could start two threads and invoke those methods in parallel.
The code will be similar to the previous example but notice that instead of invoking the callback function directly we will start a new thread and invoke the callback function within this new thread:
// Omitted code from above…
public void performAsynchronousAction(Runnable runnable) {
new Thread(() -> {
System.out.println("Processing Asynchronous Task...");
new Thread(runnable).start();
}).start();
}
The output from this operation is as follows:
Processing Asynchronous Task...
Callback executed...
The asynchronous parallel callback is useful when we don’t need the callback function to be executed immediately after the action from the performAsynchronousAction()
method.
A real-world example would be when we purchase a product online and we don’t need to wait until the payment is confirmed, the stock being checked, and all those heavy loading processes. In that case, we can do other things while the callback invocation is executed in the background.
CompletableFuture callback
Another way to use an asynchronous callback function is to use the CompletableFuture
API. This powerful API, introduced in Java 8, facilitates executing and combining asynchronous method invocations. It does everything we did in the previous example such as creating a new Thread
then starting and managing it.
In the following code example we will create a new CompletableFuture
, then we’ll invoke the supplyAsync
method passing a String
.
Next, we will create another ,CompletableFuture
that will thenApply
a callback function to execute with the first function we configured:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCallback {
public static void main(String[] args) throws Exception {
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Supply Async...");
CompletableFuture<String> execution = completableFuture
.thenApply(s -> s + " Callback executed...");
System.out.println(execution.get());
}
}
The output here is:
Supply Async... Callback executed…
Conclusion
Callbacks are everywhere in software development, vastly used in tools, design patterns, and in applications. Sometimes we use them without even noticing it.
We’ve gone through a variety of common callback implementations to help demonstrate their utility and versatility in Java code. Here are some features of callbacks to remember:
- A callback function is supposed to be executed either when another action is executed or in parallel to that action.
- A callback function can be synchronous, meaning that it must be executed right after the other action without any delay.
- A callback function can be asynchronous, meaning that it can be executed in the background and may take some time until it’s executed.
- The Observable design pattern uses a callback to notify interested entities when an action has happened.