Explain Codes LogoExplain Codes Logo

Callback functions in Java

java
callback-engineering
lambda
functional-interfaces
Anton ShumikhinbyAnton Shumikhin·Jan 31, 2025
TLDR

In Java, callbacks are achieved by implementing a single-method interface and passing an instance of an implemented class.

Example:

interface Callback { // Forget 'Hello, world!' Print 'Callback, world!' instead! void onCompleted(); } class Task { // So, you want to perform a task and get notified when it's over? No problem! void performTask(Callback cb) { // Some awesome task implementation cb.onCompleted(); } } class CustomCallback implements Callback { // This gets printed when the task sings 'I did it my way' public void onCompleted() { System.out.println("Task is done. Rest now, brave warrior!"); } } // And this is how you use it Task task = new Task(); task.performTask(new CustomCallback());

In simpler terms, the Task class will perform some work and then tell you, "Hey, I'm done!" through the onCompleted method, which acts as a callback.

Callbacks and Lambda — Like burger and fries, better together!

Java 8 introduced the convenience of lambda expressions and method references, simplifying callback implementation and promoting concise, readable code.

// Callback using a lambda, because why write ten lines when one would do? Callback callback = () -> System.out.println("Task kissed the finish line!"); Task task = new Task(); task.performTask(callback);

Built-in functional interfaces — Java's gift to callbacks

Java's java.util.function package offers commonly required functional interfaces like Consumer<T>, Supplier<T>, Function<T,R>, and Predicate<T>.

import java.util.function.Consumer; Consumer<String> onResult = result -> System.out.println("Task gave birth to result: " + result); class TaskWithResult { void performTask(Consumer<String> resultCallback) { // Task acting like a hen laying an egg String result = "Success"; resultCallback.accept(result); } } // Usage TaskWithResult taskWithResult = new TaskWithResult(); taskWithResult.performTask(onResult);

These interfaces give you the power to handle varying types of inputs and outputs, enhancing your ability to handle callbacks efficiently.

Async callbacks and error management — Dancing in a minefield

Working with asynchronous callbacks, especially in concurrent environments, can be tricky. Tools like CompletableFuture come to the rescue for handling asynchronous operations.

import java.util.concurrent.CompletableFuture; // Asynchronously whispering sweet nothings to the task CompletableFuture<Void> futureTask = CompletableFuture.runAsync(() -> { // Task secret sauce }); // Callback for when task finally decides to put a ring on it futureTask.thenRun(() -> System.out.println("Async task finished. Did you time it?"));

Ensuring you efficiently handle both success and error cases within your callbacks is key to keeping your code robust and your hair intact.

Crafting custom functional interfaces — Tailor-made suits

Sometimes, you need something tailored for your specific needs. A custom functional interface is such a bespoke callback option.

@FunctionalInterface interface TaskCompleteListener { void onTaskComplete(Result result); } // ... TaskCompleteListener listener = result -> // handle result

The @FunctionalInterface annotation ensures your interface warms up nicely with lambda expressions and method references.

Errors, exceptions, and callbacks — A love triangle

Exception handling within callbacks is paramount. It is especially important when working with volatile operations such as I/O or network requests.

interface ResultCallback { void onResult(Data result); void onError(Exception e); } // ... ResultCallback callback = new ResultCallback() { public void onResult(Data result) { // Celebrating success } public void onError(Exception e) { // Mitigating failure } };

Naturally, this pattern makes handling success and failure scenarios in your applications seamless.

Visitor pattern — A callback smorgasbord

In certain scenarios, where you have multiple callback functions, the Visitor pattern might just save you some hair-pulling.

interface VisitorCallback { void onVisitElementOne(ElementOne element); void onVisitElementTwo(ElementTwo element); } // ... VisitorCallback visitor = new VisitorCallback() { public void onVisitElementOne(ElementOne element) { // ElementOne, we've been expecting you! } public void onVisitElementTwo(ElementTwo element) { // ElementTwo, my old friend! } };

With the Visitor pattern, you can neatly parcel out different callback functionalities to handle a variety of elements or events.