Table of Contents
Overview
In Java functional programming, Consumers are incredibly versatile and valuable, particularly when it comes to performing actions on data without returning values.
Why use Consumers?
Here are the main reasons using Consumers is beneficial to your code:
Simplifying Side Effects
You may hear or read somewhere that in functional programming, side effects should be minimized. While this is true, there are situations it is necessary to have side effects (logging, updating objects, or interacting with external systems). This is where Consumers shine. They allow you to encapsulate side-effect operations in a clean and organized manner, promoting a more functional style of coding even in imperative code.
Streamlining Data Processing
Consumers are commonly used in Java streams data processing. They enable you to define actions that are performed on stream elements as they are encountered, leading to concise and expressive stream operations.
Enhanced Readability
By encapsulating actions within Consumers, you can make your code more readable and self-documenting. Rather than having complex logic embedded within your code, you express your intent through the actions of the Consumer, making it easier for others (and your future self) to understand the code.
The Consumer Interface in Java
The Consumer interface has a single abstract method named accept
, which accepts an argument of type T and returns void. This method’s signature is straightforward:
void accept(T t);
Enough theory, let’s try some examples.
Consumer examples
Let’s consider the following scenario: you have a system that monitors the status of a number of sites. When any site is down (HTTP header >= 400), you want to send a notification to the responsible people.
record SiteStatus(int httpStatus, String url) { } public static void main(String[] args) { var allSitesStatuses = List.of( new SiteStatus(200, "http://www.google.com"), new SiteStatus(400, "http://www.facebook.com"), new SiteStatus(200, "http://www.twitter.com"), new SiteStatus(404, "http://www.microsoft.com") ); Consumer<SiteStatus> siteStatusReporter = (SiteStatus status) -> { System.out.println("HTTP status: " + status.httpStatus()); System.out.println("URL: " + status.url()); }; //report sites that is down allSitesStatuses.stream() .filter(status -> status.httpStatus() >= 400) .forEach(siteStatusReporter); }
Running the code above would produce the following result:
Consumer as methods’ arguments
The consumer above doesn’t do much. It just logs the message on the screen. What if you need to support SMS or Telegram notifications?
First, let’s create a method that accepts a consumer:
private static void notify(SiteStatus status, Consumer<SiteStatus> reporter) { reporter.accept(status); }
Now, for each type of notification, you create a consumer accordingly and pass such consumers to this method:
public static void main(String[] args) { var allSitesStatuses = List.of( new SiteStatus(200, "http://www.google.com"), new SiteStatus(400, "http://www.facebook.com"), new SiteStatus(200, "http://www.twitter.com"), new SiteStatus(404, "http://www.microsoft.com") ); Consumer<SiteStatus> smsReporter = (SiteStatus status) -> System.out.println("📟 SMS: " + status); Consumer<SiteStatus> telegramReporter = (SiteStatus status) -> System.out.println("💬 Telegram: " + status); allSitesStatuses.stream().filter(t -> t.httpStatus >= 400).forEach(status -> notify(status, smsReporter)); allSitesStatuses.stream().filter(t -> t.httpStatus >= 400).forEach(status -> notify(status, telegramReporter)); }
The code above would produce the following results:
Chaining Consumers
In the example above, I called the function notify
multiple times when I want to send multiple types of notifications.
However, I can use consumer chaining to make the call only once.
public static void main(String[] args) { var allSitesStatuses = List.of( new SiteStatus(200, "http://www.google.com"), new SiteStatus(400, "http://www.facebook.com"), new SiteStatus(200, "http://www.twitter.com"), new SiteStatus(404, "http://www.microsoft.com") ); Consumer<SiteStatus> smsReporter = (SiteStatus status) -> System.out.println("\uD83D\uDCDF SMS: " + status); Consumer<SiteStatus> telegramReporter = (SiteStatus status) -> System.out.println("\uD83D\uDCAC Telegram: " + status); var chainedConsumer = smsReporter.andThen(telegramReporter); allSitesStatuses.stream().filter(t -> t.httpStatus >= 400).forEach(status -> notify(status, chainedConsumer)); }
Using this chaining mechanism, you can chain as many consumers as you need to.
The chaining code example still sends both Telegram and SMS notifications. However, the ordering of the messages is quite different:
Other Consumers
In addition to the Consumer interface, there are also other Consumers created for your convenience.
I’ve covered the BiConsumer interface here. In this post, let’s explore other Consumers in the java.util.function package.
DoubleConsumer, IntConsumer, LongConsumer
These consumers are created to work directly on the primitive types (double, int, long). These consumers help prevent the performance overhead of boxing/unboxing.
Other than that, these interfaces have the same method and default method as the Consumer interface (accept & andThen).
Creating and using these consumers would be trivial to you at this point:
private static void numberConsumers() { IntConsumer intConsumer = System.out::println; LongConsumer longConsumer = System.out::println; DoubleConsumer doubleConsumer = System.out::println; intConsumer.accept(1); longConsumer.accept(1L); doubleConsumer.accept(1.0); }
ObjDoubleConsumer, ObjLongConsumer and ObjIntConsumer
These interfaces are used when you want to operate on a combination of an object of type T
and a primitive (int, long, double) value.
Consider this scenario: You are running an e-commerce site and to adjust to the input price changes, you want to increase the price of all products by 10%. Using an ObjDoubleConsumer would simplify your tasks.
Let’s first create the Product class:
static class Product { private final String name; private double price; public Product(String name, double price) { this.name = name; this.price = price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + '}'; } }
Now, let’s use an ObjDoubleConsumer to handle the price increase logic:
public static void main(String[] args) { var products = List.of( new Product("iPhone 3000", 8000), new Product("iPad", 1000), new Product("MacBook", 2000) ); //use a DoubleObjConsumer to increase the price of a product by 10% ObjDoubleConsumer<Product> priceIncrease = (Product product, double percentage) -> { double newPrice = product.price * (1 + (percentage / 100.0)); product.setPrice(newPrice); }; products.forEach(product -> priceIncrease.accept(product, 10)); //print the new list products.forEach(System.out::println); }
The code above would produce the expected result:
Conclusion
in this post, I introduced you to the Consumer and other Consumer-related interfaces in java.util.function package.
The Consumer functional interface is a fundamental part of Java’s functional programming toolkit, designed for performing actions or operations on data.
They are versatile and can be used for logging, printing, data modification, and many other tasks involving side effects. They promote code readability, reusability, and modularization by encapsulating actions within functions.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.