Java Consumer Functional Interface Tutorial

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:

java consumer example

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:

Using consumer as methods' arguments

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:

Chaining consumer

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:

ObjDoubleConsumer example

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.

Leave a Comment