rafael_del nero
Java Developer

How to use callbacks in Java

how-to
Feb 28, 20237 mins
JavaSoftware Development

Learn how to use synchronous and asynchronous callbacks in Java—including callbacks with lambda expressions, CompletableFuture, and more.

Red rotary phone, ringing, calling, callback
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.
rafael_del nero
Java Developer

Rafael del Nero is a Java Champion and Oracle Ace, creator of the Java Challengers initiative, and a quiz master in the Oracle Dev Gym. Rafael is the author of "Java Challengers" and "Golden Lessons." He believes there are many techniques involved in creating high-quality software that developers are often unaware of. His purpose is to help Java developers use better programming practices to code quality software for stress-free projects with fewer bugs.

More from this author