The Consumer interface

The Consumer interface denotes a lambda expression that performs an action on values supplied to it. It is expected that the action may have side effects: in other words, it may modify the data structure passed in, or potentially have some other effect on data or the environment outside the data passed in.

As we will see below, Consumers are commonly used with the forEach() method on a Stream or collection.

Lambda expressions that can be used as Consumers

A lambda expression is a Consumer if:

For example, the following lambda expressions can all be Consumers:

(obj) -> list.add(obj)
	
(msg) -> System.out.println(msg)

(err) -> ErrorHandler.register(err)

Common uses of Consumer

One of the key methods that takes a Consumer is the forEach() methods on Stream. This means that we can take a stream and terminate with a call to forEach(), passing in a lambda expression that represents the action that we would like to perform on each item in the stream. For example, if we want to take items from a list of strings, filter them on strings less than 10 characters in length, and then print the matching strings, we can accomplish this as follows:

List<String> strings = ...

strings.stream()
  .filter((s) -> s.length() < 10)
  .forEach((s) -> System.out.println(s));

A call to forEach() terminates the Stream. In other words, this is the method that actually causes the items to be pulled one by one from the list and the lambda expression invoked on each of the items in the list.

Performing an action on all items in a list or other collection is a very common operation. Therefore, if you don't need to filter the items in any way but simply perform the given action on all items in the list, there is a forEach() method that you can call directly:

strings.forEach((s) -> System.out.println(s));

Passing a method refernece in as a Consumer

Any method that takes a single object as its argument can be used directly as a Consumer by passing in an appropriate method reference and this is in fact a very common way to specify the required action with forEach() or another method that takes a Consumer. For example, to print the items in a list, we can simply write:

strings.forEach(System.out::println);

To take all items from one list and add them to another map, we can write:

strings.forEach(map::add);

Processing items in parallel

If you have a large number of items to process, then these can be passed to your Consumer in parallel by calling forEach() on a parallel stream as follows:

items.parallelStream()
  .forEach((x) -> ...;	

This makes it very easy to process items in parallel using the Stream API. But it is the programmer's responsibility to ensure that the items in question can be safely process in parallel (see below).

Modifying data with a Consumer

Consumers are special in the sense that it is expected that their action could have side effects: in other words, that they might modify either the object passed in or some other object that is visible to the lambda expression.

Where possible, it is good practice for the Consumer to modify only the data passed in. But in reality, a Consumer may need to modify some global structure or perform a global action such as logging. At the same time, one of the benefits of the Stream API is the ease with which parallel processing can be performed, as we illustrated in the previous example. If you do use Consumer with a parallelStream(), you need to take care if multiple invocations of the consumer will access shared data:

The Consumer counterpart: Supplier

The Supplier interface is in a sense the opposite of a Consumer: it takes no input parameters but generates a value. As we will see on the next page, it is therefore often used for lazy initialisation.


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.