Learn how to use synchronous and asynchronous callbacks in Java—including callbacks with lambda expressions, CompletableFuture, and more. Credit: BigBlueStudio/Shutterstock 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. Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos