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:
- it takes a single onbject as its parameter;
- it does not return a value.
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:
- within your Consumer, it is the programmer's responsibility to apply appropriate synchronization or locking,
or ensure that the data structure being modified is thread-safe;
- when used with a parallelStream(), you should not make any assumptions about the order in which the Consumer
will be invoked on different objects;
- to actually gain the benefits of parallel processing, you should try to avoid contention between the parallel calls as much as possible
(e.g. by trying to avoid multiple Consumers working on shared objects if they can work on their own local copies).
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.