Callbacks in Java and Kotlin
Quite frequently there can be a situation in any project when simply passing a value to a function (method) is not enough. For example, some action should be run asynchronously at the end of processing. Or the function (method) may operate in a different context and may not know what to do with the data passed to it.
In such cases, a very elegant and convenient solution is to use callbacks. Callback is a function (method) that is passed as an argument to another function (method). First, any callback can be a free-form function and does not need to adhere to any interface. However, using existing Java interfaces is very convenient as they are already defined.
Available Java interfaces
There are quite a lot of predefined interfaces, the basic ones are:
- Consumer<T> (consumes a value of a predefined type and provides no response)
- Supplier<T> (provides a value of a predefined type without asking for any input parameters)
- Function<T,R> (requires an input of type T, provides a value of type R)
We will take a look at Consumer<T> and Function<T,R> because Supplier<T> usually can be replaced by a provided value.
Java consumer callback example
In Java a simple value consumer will look like this:
public class PrintingMessageConsumer implements Consumer<String> {
@Override
public void accept(final String message) {
System.out.println("Consumed: " + message);
}
}
Then we can also define a class to which this consumer will be passed:
public class MessageService {
public void processMessage(final String message, final Consumer<String> messageConsumer) {
// do work...
messageConsumer.accept(message);
}
}
And in some other place everything will be aligned similar to this:
public class MessageService {
public class Main {
public static void main(String[] args) {
final String message = "sample message";
final MessageService messageService = new MessageService();
final Consumer<String> messageConsumer = new PrintingMessageConsumer();
messageService.processMessage(message, messageConsumer);
}
}
As you would expect, PrintingMessageConsumer will print the following message to the console:
Consumed: sample message
Kotlin consumer callback example
In Kotlin the code that gives the same outcome will look like this:
class PrintingMessageConsumerKt : Consumer<String> {
override fun accept(message: String) {
println("Consumed: $message")
}
}
class MessageServiceKt {
fun processMessage(message: String, messageConsumer: Consumer<String>) {
// do work...
messageConsumer.accept(message)
}
}
fun main(args: Array<String>) {
val message = "sample message"
val messageService = MessageServiceKt()
val messageConsumer = PrintingMessageConsumerKt()
messageService.processMessage(message, messageConsumer)
}
Java function callback example
A function callback will be very similar to a consumer callback. First, we need to define a function:
public class IdentityFunction implements Function<String, String> {
@Override
public String apply(String message) {
return message;
}
}
Then we slightly modify MessageService from the previous example:
public class MessageService {
public String processMessage(final String message, final Function<String, String> messageFunction) {
// do work...
return messageFunction.apply(message);
}
}
And the Main class:
public class Main {
public static void main(String[] args) {
final String message = "sample message";
final MessageService messageService = new MessageService();
final Function<String, String> messageFunction = new IdentityFunction();
final String result = messageService.processMessage(message, messageFunction);
System.out.println("Identity function result: " + result);
}
}
This will, of course, produce the following output:
Identity function result: sample message
Kotlin function callback example
Kotlin code will look like this:
class IdentityFunctionKt : Function<String, String> {
override fun apply(message: String): String = message
}
class MessageServiceKt {
fun processMessage(message: String, messageFunction: Function<String, String>): String {
// do work...
return messageFunction.apply(message)
}
}
fun main(args: Array<String>) {
val message = "sample message"
val messageService = MessageServiceKt()
val messageFunction = IdentityFunctionKt()
val result = messageService.processMessage(message, messageFunction)
println("Identity function result: $result")
}
Simplifications of Kotlin code
Additionally, Kotlin is a very flexible and dynamic language. So, the same outcome can be achieved with as little code as:
fun main(args: Array<String>) {
val message = "sample message"
val messageService = MessageServiceKt()
messageService.processMessage(message, Consumer {
// do work...
println("Consumed: $message")
})
val result = messageService.processMessage(message, Function {
// do work...
message
})
println("Identity function result: $result")
}
This includes both consumer and callback and function callback and does not require any additional class creation.
Conclusion
That is all there is regarding callback functions in Java and Kotlin. I would like to point out that callbacks are free-form functions and there is no need to follow certain guidelines or implement a certain interface. However, implementing an interface is a good practice and would be highly appreciated by your colleagues who will try to read and understand your code 🙂 Happy coding!