OCP Question 33, Explanation

Given:

public class Item {
    int id; int price;
    public Item (int id, int price) {
        this.id = id;
        this.price = price;
    }
    public String toString() { return id + " : " + price; }
}

and the code fragment:

List<Item> inventory = Arrays.asList(new Item(1, 10),
    new Item(2, 15),
    new Item(3, 20));
Item item = inventory.stream()
                     .reduce(new Item(4, 0), (x, y) -> {
                             x.price += y.price;
                             return new Item(x.id, y.price);});
inventory.add(item);
inventory.stream()
         .parallel()
         .reduce((x, y) -> x.price > y.price ? x : y)
         .ifPresent(System.out::println); 

What is the result?

A. 4 : 45
B. 4 : 0
C. 4 : 20
D. 1 : 10
2 : 15
3 : 20
4 : 45
E. The program prints nothing

 

The correct answer is E.

 

Since we see no filter(), and reduce()’s logic does guarantee a returned value, ifPresent() should have something to work with thus pushing us towards choosing among options A though D. The program itself, however, is a bit too complex; just take a look at the first reduction, which returns an Item whose id and price fields actually come from different objects; this is a good indicator of a potential comperr or RTE. And indeed this is our case because any List created with the Arrays.asList() method is structurally immutable; even the OCA-related Nailing 1Z0-808 mentioned this very fact on multiple occasions.

As a result, the code throws an UnsupportedOperationException when trying to add a newly created element to inventory. The wording of option E is, technically speaking, correct because it’s the JVM itself rather than the program who prints the exception message.

Populating a structurally modifiable inventory leads to printing 4 : 20:

class Item {
    int id; int price;
    public Item (int id, int price) {
        this.id = id;
        this.price = price;
    }
    public String toString() { return id + " : " + price; }
}

class Test{
    public static void main(String[] args) {
//       List<Item> inventory = Arrays.asList(new Item(1, 10),
//           new Item(2, 15),
//           new Item(3, 20));

        List<Item> inventory = new ArrayList<>();
        inventory.add(new Item(1, 10));
        inventory.add(new Item(2, 15));
        inventory.add(new Item(3, 20));

        Item item = inventory.stream()
                             .reduce(new Item(4, 0), (x, y) -> {
                                     x.price += y.price;
                                     return new Item(x.id, y.price);});
        inventory.add(item);
        inventory.stream()
                 .parallel()
                 .reduce((x, y) -> x.price > y.price ? x : y)
                 .ifPresent(System.out::println);             // 4 : 20
    }
}

Would you like to see why? Okay, let’s analyze how the inventory’s stream is being operated on here. Probably, the biggest challenge is getting a solid grasp of how the reduce() method actually works.

The java.util.stream.Stream interface defines three reduce() methods for ordinary reduction:

and two more collect() methods for the so-called mutable reduction:

The differences between the two reduction approaches are not important for our exam; still, it might be a good idea to watch Angelika Langer’s presentation on this subject.

Okay, our Problem uses the second version of reduce(), the one with an identity arg. From the package java.util.stream description, section Reduction operations: “The identity element is both an initial seed value for the reduction and a default result if there are no input elements”.

Reductions always operate on each and every element of the stream in question. In our case reduce() first takes identity (i.e., the object created by new Item(4,0)) and the starting element of the stream (i.e., new Item(1,10)), does its thing by using these two objects (the lambda expression calls them x and y, respectively), and then returns a single element (i.e., new Item(x.id, y.price)). In other words, two elements are reduced to one. Finally, the reduction’s result gets assigned to the item variable.

Then reduce() grabs identity again, looks at the next element in the stream (i.e., new Item(2, 15)), reduces this pair to another Item object according to the lambda expression’s logic, and assigns this object to item. After that the process is repeated for the last time because this particular stream contains only three elements.

Please note that our reduce() does not do its thing like a true blackbox, performing everything inside itself and only after that producing some final result. No, the method returns a result for each pair consisting of identity and every successive element until the stream is exhausted.

Illustration:

final List<Item> reduced = new ArrayList<>();
List<Item> list = Arrays.asList(new Item(1,1),
        new Item(2,2),
        new Item(3,3));
list.stream()
    .reduce(new Item(4,0), (x,y) -> {
       Item item = new Item(x.id, y.price);
       reduced.add(item);
       return item;
    });
System.out.println(reduced); // [4 : 1, 4 : 2, 4 : 3]

The above example populates reduced with each returned result. As we can see, the x object is always the same (identity), and y corresponds to the elements in list, one after another.

By the way, we practically reproduced how the collect() method works, and even demonstrated the pitfalls of mutability, something that Angelika Langer talks about in her presentation. Also please note the final modifier on reduced: lambdas may reference only final or effectively final variables when these vars have been declared outside the lambda expression. Actually, in our code this final modifier isn’t even necessary because we don’t change reference to the reduced object; we only change its contents by adding more and more new elements. Just keep this point in mind, OK? because there’s a question on the exam that’ll ask you about this…

Fine; now that we know how reduce() works, it’s time to take a look at the last block of stream operations in our Problem 32:

inventory.stream()
         .parallel()
         .reduce((x, y) -> x.price > y.price ? x : y)
         .ifPresent(System.out::println);

Here we have the first version of reduce(), the one with a BinaryOperator arg. Since this time there’s no guaranteed value to be returned (no default identity), the return type is an Optional, which explains why we see ifPresent(), instead of, say, forEach().

BinaryOperator means that the functional method of this interface (that is, apply()) takes two args of the same type, does something to them – or maybe with them, or maybe even doesn’t touch them at all, – and then returns some value of the exact same type; that’s why it is called operator. Functions, on the other hand, take in and return different types. It was just a reminder, alright?

So, our reduce() compares two Item objects and returns an Optional wrapped around the Item object whose price is higher, and then ifPresent() prints this Optional’s contents. Which we can even read thanks to the overridden toString() in the Item class.

Leave Comment

Your email address will not be published. Required fields are marked *