Quiz Yourself: Using Core Functional Interfaces: Consumer (Advanced)

The consumer interface can be confusing, even for advanced coders.

April 6, 2020 | Download a PDF of this article
More quiz questions available here

If you have worked on our quiz questions in the past, you know none of them is easy. They model the difficult questions from certification examinations. The “intermediate” and “advanced” designations refer to the exams rather than to the questions, although in almost all cases, “advanced” questions will be harder. We write questions for the certification exams, and we intend that the same rules apply: Take words at their face value and trust that the questions are not intended to deceive you but to straightforwardly test your knowledge of the ins and outs of the language.

The objective here is to use core functional interfaces including Predicate, Consumer, Function, and Supplier. This particular quiz focuses on consumers.

Given the code fragment:


Stream.of(1).peek(
    ((Consumer<Integer>)(i1)->{i1 = i1 + 1;})
     .andThen((i2)->{i2 = i2 + 2;}))
  .forEach(System.out::print);

What is printed to the console? Choose one.

A. 1
B. 2
C. 3
D. 4
 

Answer. The Stream.peek(...) method is described in the API documentation as returning a stream consisting of the elements of the stream and additionally performing the provided action on each element as elements are consumed from the resulting stream.

The method takes a Consumer as its argument. The intention is that the original stream is unmodified, while each element will be passed into the Consumer. Typically, the method might be used to observe (such as print) the items in the stream to help with debugging.

This question hinges on the effects performed in the Consumer and whether they affect the data in the stream. In all stream operations, it’s generally best to avoid modifying elements in a stream. Instead the preferred behavior is to create a new element representing a change and pass that along the stream to the next operation. Despite that advice, there are many situations where regular Java syntax can result in behavior that breaks this guidance and mutates stream data.

The java.util.function.Consumer interface’s abstract method has this signature:


void accept(T t);

Because this method declares a void return type, there are only two mechanisms by which it could mutate any data. First, if the argument is a mutable reference type (such as StringBuilder, but not String), it can change the contents of the object referred to by its argument. Second, if it uses an external reference to a mutable object, it could change such an object’s contents.

It should be clear that the code that implements Consumer for this question has no external references, so it cannot change anything except perhaps its argument, if that is a mutable object. So, the next thing to determine is the argument type. It might look like a primitive int, but it is in fact an object, specifically an autoboxed Integer object. Notice that the source of the stream is the Stream.of factory method. This makes an object stream, so the int must be boxed. If this had been created using IntStream.of, it would have been a stream of primitives.

The next observation is that Integer objects are immutable, and code of the following form does not mutate the originally created object; instead it creates a new Integer and changes the reference value in i1 to refer to it:


Integer i1 = 1;
i1 = i1 + 1;

This is directly comparable to this code with String types:


String s = "Hello";
s = s + " world! ";

From this, you can conclude that in this case, the stream that is returned from the peek must consist of the single original and unmodified object and, therefore, the output is 1, which means that option A is correct and options B, C, and D are incorrect.

It might be interesting to investigate the code in the Consumer a little closer. Let’s extract the key elements and lay them out to make the behavior easier to discuss:


final int ONE = 1;
final int TWO = 2;
Consumer<Integer> c1 = (i1)->{i1 = i1 + ONE;};
Consumer<Integer> c2 = (i2)->{i2 = i2 + TWO;};        
Consumer<Integer> cFinal = c1.andThen(c2);
cFinal.accept(Integer.valueOf(1));

The Consumer interface has a default method: andThen(Consumer c2). It creates a new Consumer that combines the behavior of the original two. Invoking the behavior embodied in cFinal passes the argument of that invocation (which is the boxed Integer(1) from the stream) to c1, which creates a new Integer(2) that promptly goes out of scope when the void method returns.

The cFinal operation then takes the original Integer(1) and invokes c2, using it as the argument. That invocation adds TWO and creates another new Integer(3), which also goes out of scope at that method’s return. So, as noted above, neither of the method-local Integer objects is placed in the stream, and both of them are potentially eligible for garbage collection as soon as the methods that created them have completed.

Stylistically, notice that the body of the Consumer lambdas were changed to add final variables ONE and TWO rather than the literal constants 1 and 2. With the form used in the original code, it’s really easy to see what looks like two identical regions of code and merely assume that they’re actually identical. The longer form of the code makes that difference more obvious and might be preferred in real code in similar situations.

The correct answer is option A.

Simon Roberts

Simon Roberts joined Sun Microsystems in time to teach Sun’s first Java classes in the UK. He created the Sun Certified Java Programmer and Sun Certified Java Developer exams. He wrote several Java certification guides and is currently a freelance educator who publishes recorded and live video training through Pearson InformIT (available direct and through the O’Reilly Safari Books Online service). He remains involved with Oracle’s Java certification projects.

Mikalai Zaikin

Mikalai Zaikin is a lead Java developer at IBA IT Park in Minsk, Belarus. During his career, he has helped Oracle with development of Java certification exams, and he has been a technical reviewer of several Java certification books, including three editions of the famous Sun Certified Programmer for Java study guides by Kathy Sierra and Bert Bates.

Share this Page