Deferred Execution with Java’s Consumer
In an earlier blog post (“Deferred Execution with Java’s Supplier“) I referenced Cay Horstmann‘s statement in the book “Java SE8 for the Really Impatient” regarding lambda expressions, “The point of all lambdas is deferred execution.” Horstmann wrote an article called “Lambda Expressions in Java 8” for Dr. Dobb’s magazine in its final year in which he wrote a similar statement using different terminology, “A lambda expression is a block of code that you can pass around so it can be executed later, just once or multiple times.”
In that earlier post, I looked at how the standard functional interface Supplier is used with lambda expressions in the JDK to support deferred execution for cases where a single value is “supplied only when necessary” and without any argument passed to it. In this post, I focus on JDK-provided examples of using the Consumer standard functional interface to “consume” or “process” a particular code block “only when necessary.” Whereas the Supplier
accepts no arguments and returns exactly one response, the Consumer
accepts one or more arguments and returns no response. The method invoked on a Supplier
is the get()
method and it is the accept(T)
method for a Consumer
. By definition, the Consumer
is expected to have “side effects” as it “consumes” the provided code block.
There are numerous Consumer
-style standard functional interfaces supplied in the java.util.function package. None of these returns a result (that’s why they’re consumers!), but they differ in the number and types of arguments they accept (but they all accept at least one argument). These are listed here:
- Consumer – General
Consumer
that accepts a single argument and will be the center of attention for most of this post’s examples. - BiConsumer – Accepts two arguments instead of one (“two-arity specialization of Consumer“)
- DoubleConsumer – Specialized Consumer intended for primitive
double
s - IntConsumer – Specialized consumer for primitive
int
s - LongConsumer – Specialized Consumer intended for primitive
long
s - ObjDoubleConsumer – Specialized Consumer that accepts two arguments, with the first of type
Object
and the second of typedouble
- ObjIntConsumer – Specialized Consumer that accepts two arguments, with the first of type
Object
and the second of typeint
- ObjLongConsumer – Specialized Consumer that accepts two arguments, with the first of type
Object
and the second of typelong
The remainder of this post will look at a subset of the JDK uses of Consumer
and related classes to help demonstrate how and when they are useful.
Peeking at Flow of Stream Elements
In the blog post “Peeking Inside Java Streams with Stream.peek,” I discussed the intermediate operation Stream.peek(Consumer) that can be used to view the flowing elements of a stream. This can be very useful for understanding what the various stream operations are doing to their respective stream elements. A common way to do this is to have the Consumer
provided to the peek
method be a call to System.out.println that prints the currently processed stream element to standard output (or log the element or print it to standard error). An example of this is provided in the Javadoc documentation for the Stream.peek(Consumer) method:
Stream.of("one", "two", "three", "four") .filter(e -> e.length() > 3) .peek(e -> System.out.println("Filtered value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList());
Because the various overloaded versions of the println(-)
method accept a parameter but do not return anything, they fit perfectly with the “Consumer” concept.
Specifying Action Upon Iterated Stream Elements
While Stream.peek(Consumer)
is an intermediate operation, Stream provides two other methods that accept a Consumer
that are both terminal operations and are both “for each” methods. The method Stream.forEach(Consumer) is a method that performs the action specified by the provided Consumer
in an “explicitly nondeterministic” manner on the stream’s elements. The method Stream.forEachOrdered(Consumer) performs the action specified by the provided Consumer
in “the encounter order” of the stream if that stream has an encounter order. In both methods’ cases, the Consumer
-based “action” should be “non-interfering.” Both methods are demonstrated below.
Set.of("one", "two", "three", "four") .stream() .forEach(i -> out.println(i.toUpperCase())); Stream.of("one", "two", "three", "four") .forEach(i -> out.println(i.toUpperCase())); List.of("one", "two", "three", "four") .stream() .forEachOrdered(i -> out.println(i.toUpperCase())); Stream.of("one", "two", "three", "four") .forEachOrdered(i -> out.println(i.toUpperCase()));
The above examples look and very similar. The most obvious situation in which forEach
could lead to dramatically different results than forEachOrdered
is when parallel stream processing is employed. In that case, it makes most sent to use forEach
instead of forEachOrdered
.
Specifying Action Upon Iterable Elements
The previous code examples showed using Stream.forEach(Consumer)
methods to iterate a stream. The examples also demonstrated doing this against a Set
and List
by first calling stream()
on these collections. There are convenience methods, however, that are defined by Iterable and implemented by these collection implementations which accept a Consumer
and allow for iteration of that collection using the forEach
method. Examples of this are shown in the next code listing.
Set.of("one", "two", "three", "four") .forEach(i -> out.println(i.toUpperCase())); List.of("one", "two", "three", "four") .forEach(i -> out.println(i.toUpperCase()));
Although I used collections in my example above, anything that implements Iterable will generally support the forEach
method (or be in violation of the interface’s advertised contract).
Specifying Action Upon Iteration of Map Entries
Although Java’s Map interface does not extend the Iterable
interface like Set
and List
do, the Java Map
was still provided with a similar capability to specify a consumer to “consume” each entry in the Map
. Because a Map
has two input arguments (key and value), its forEach
method accepts a BiConsumer instead of the Consumer discussed so far in this post. A simple example is shown next.
Map.of("Denver", "Colorado", "Cheyenne", "Wyoming", "Salt Lake City", "Utah", "Boise", "Idaho") .forEach((c, s) -> out.println(c + " is the capital of " + s));
Walking the Stack
The StackWalker is a welcome addition to JDK 9 that provides a thread-safe approach to perusing a stack trace and is a significant improvement over the StackTraceElement approach. It’s arguably more common for developers to use StackWalker.walk(Function), but this post is about Consumer
and so the focus is on StackWalker.forEach(Consumer). This method is similar to the previously discussed Stream.forEach
and Iterable.forEach
methods and is demonstrated in the next code listing.
StackWalker.getInstance().forEach(out::println);
Although there are many more JDK uses of Consumer, of BiConsumer, and of the other types of standard Consumer-style functional interfaces, the last examples I’ll cover in this post come from the Optional class.
Applying Only When Present
The methods Optional.ifPresent(Consumer) and Optional.ifPresentOrElse(Consumer) defer the execution of the provided Consumer
s such that the provided Consumer
will only be invoked if the Optional
is not “empty” (contains a non-null
value). This is a simple but powerful concept and the simplistic and contrived examples show how they work.
public void demonstrateOptionalIfPresent() { getMiddleName(true).ifPresent(n -> out.println("Middle Name: " + n)); } public void demonstrateOptionalIfPresentOrElse() { getMiddleName(false).ifPresentOrElse( n -> out.println("Middle Name: " + n), () -> displayMissingMiddleName()); } private Optional<String> getMiddleName(final boolean present) { return present ? Optional.of("Wayne") : Optional.empty(); } private void displayMissingMiddleName() { out.println("No middle name provided!"); }
As the above code listing demonstrates, both Optional.ifPresent
and JDK 9-introduced Optional.ifPresentOrElse()
only invoke the provided Consumer
if the Optional
is not empty. If the Optional
is empty, the ifPresent
method does nothing and the ifPresentOrElse
invokes the second argument (a Runnable).
The standard Java functional interfaces that accept one or more arguments and return no result include the general Consumer
as well as some specialized consumers. These are useful for deferring execution until a given condition occurs (such as being iterated upon or being determined to be present) and the behavior to apply when that condition occurs involves one or more input arguments and no need to provide a response. The source code examples shown in this post are available on GitHub.
Published on Java Code Geeks with permission by Dustin Marx, partner at our JCG program. See the original article here: Deferred Execution with Java’s Consumer Opinions expressed by Java Code Geeks contributors are their own. |