Quiz yourself: Performing reductions with Java streams using collectors and grouping

The trick is to visualize how stream elements travel along the pipeline.

February 8, 2021 | 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. 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.

Given the following enums and class:


enum Level {ASSOCIATE, PROFESSIONAL}
enum Version {JAVA_8, JAVA_11}
public class JavaExam {
    Level lvl;
    Version ver;
    String code;
    public JavaExam(Level l, Version v, String cd) {lvl=l;ver=v;code=cd;}
    … // getters and setters are omitted for brevity
}

And the given this Java method code fragment:


List<JavaExam> exams = List.of(
    new JavaExam(Level.ASSOCIATE, Version.JAVA_8, "1Z0-808"),
    new JavaExam(Level.PROFESSIONAL, Version.JAVA_8, "1Z0-809"),
    new JavaExam(Level.ASSOCIATE, Version.JAVA_11, "1Z0-815"),
    new JavaExam(Level.PROFESSIONAL, Version.JAVA_11, "1Z0-816"),
    new JavaExam(Level.PROFESSIONAL, Version.JAVA_11, "1Z0-819")
);
var res = exams.stream()
    .filter(e->(e.getVer() == Version.JAVA_11 ||
                Version.JAVA_8.equals(e.getVer())))
    .collect(
        Collectors.groupingBy(JavaExam::getVer, 
            Collectors.groupingBy(e -> e.getLvl(), 
                Collectors.mapping(JavaExam::getCode, 
                    Collectors.toList())))); // line n1
System.out.print(
    res.get(Version.JAVA_11)
        .get(Level.PROFESSIONAL)
            .get(1)); // line n2

Which is the result? Choose one.

A. Compilation fails in the collect method arguments that end at line n1.

B. Compilation succeeds and a runtime exception is thrown in the print method arguments that end at line n2.

C. 1Z0-819

D. 1Z0-816

E. JavaExam@7d4793a8

Answer. This question is quite a bit more complicated than questions on the older exams, but Java certification candidates should know that this is representative of questions in the 1Z0-819 exam that was released in September 2020.

The new 1Z0-819 exam has an objective: “Perform decomposition and reduction, including grouping and partitioning on sequential and parallel streams.” So, be prepared to see questions on standard collectors that are provided by the java.util.stream.Collectors class.

Take a look at the code for this question. It starts by initializing an unmodifiable list of JavaExam objects. The list is then used as the source of a stream.

Next, the stream is filtered. The filter method’s argument tests the JavaExam objects to see if the version of Java is either version 8 or 11. The code is a little odd, since it uses identity comparison (the == operator) to determine if the version is 11, but it uses the equals method to determine if the version is 8. Although it’s strangely inconsistent, all of this is actually just a distraction.

The versions are identified by enum values, which means that both approaches are functionally equivalent. This is because each value of an enum is guaranteed to be a singleton, so no matter how many times you refer to Version.JAVA_11, it will always be the same object. Further, because you filter based on the version being either 8 or 11, all stream elements pass unhindered down the stream.

The collect(Collector c) method performs a mutable reduction operation. The results are determined by the specific implementation of the Collector interface that is provided as an argument. For example, it might produce a collection of stream elements, such as a list or a map. Implementations of the Collector interface can be provided by the application programmer, but many are provided by the final class java.util.stream.Collectors. This class provides many static “factory” methods which return predefined Collector instances that perform useful collection operations.

One popular behavior provided by factories in the Collectors class is that of grouping elements into a map. The behaviors (there are several variations) are somewhat similar to that of a SQL “group by” operation. They are provided by factory methods with the name groupingBy.

The simplest form uses a function called a classifier to create a key from each element in the stream. Then an entry in the map is located (or created, for the first time this key is found) for that key. In this simplest form, the value part of the Map’s entry is a List, and each stream element is then added to that list, which will ultimately contain all the elements that produced the same key.

In this simplest form of the operation, the classifier function is passed as the first parameter into the groupingBy method and is the only argument. However, overloaded groupingBy factories exist that allow the “postprocessing” of the elements and the creation of result structures other than only a list. This postprocessing is akin to stream processing in concept. To take advantage of this, a second parameter is provided to the groupingBy method. This second parameter is itself a Collector implementation. To support the idea of performing stream-like processing, there are predefined collector factories that allow operations such as filtering, mapping, counting, summing, and more.

In this example, the code continues with a second grouping operation. This makes a finer-grained separation of the elements. At that point, the code performs a mapping operation to extract the exam code, and then it collects the elements into List objects.

This is all a bit complex, so you should visualize how elements travel along the pipeline. The following are not exact representations; rather they are pseudocode:

  1. All the elements pass unchecked through the filter.
  2. After the first groupingBy, you get a map with two keys: JAVA_8 and JAVA_11. The elements that are passed along for subsequent processing and inclusion in the value part of the map are JavaExam objects. The particular objects are enumerated in square brackets and identified by their exam codes in angle brackets, as follows:

     

    
     [{JAVA_8  =>[<1Z0-808>, <1Z0-809>]}, 
      {JAVA_11 =>[<1Z0-815>, <1Z0-816>, <1Z0-819>]}]
    
    
  3. The second groupingBy processor operates on the elements passed along as matching particular keys in the first grouping step. These produce two more maps (one for each value of the first map). Each of these two maps has two keys: ASSOCIATE and PROFESSIONAL. And again, the JavaExam objects that create those keys are passed along to a third processing stage toward the creation of a value for the map. Once again, these values are identified by their exam codes in angle brackets, as follows:

     

    
     [{JAVA_8  =>[
        {ASSOCIATE =>  [<1Z0-808>]}, 
        {PROFESSIONAL =>[<1Z0-809>]}
     ]}, 
      {JAVA_11 =>[
         {ASSOCIATE =>    [<1Z0-815>]}, 
         {PROFESSIONAL => [<1Z0-816>, <1Z0-819>]}
     ]}]
    
    
  4. The mapping operation replaces the JavaExam objects with just their exam codes, so this time the result actually has simple string objects that are passed for further processing to create a final result value. So, here, the values are actual strings, and they are represented in double-quote string format, as follows:

     

    
     [{JAVA_8  =>[
        {ASSOCIATE =>  ["1Z0-808"]}, 
        {PROFESSIONAL =>["1Z0-809"]}
     ]}, 
      {JAVA_11 =>[
         {ASSOCIATE =>    ["1Z0-815"]}, 
         {PROFESSIONAL => ["1Z0-816", "1Z0-819"]}
     ]}]
    
    
  5. The final processing step is the toList() operation. This takes the elements that are passed to it and adds them to a java.util.List object, such that the final value for each element of these second-level maps is, in fact, a list containing the elements passed to it. Thus, in the illustration in step 4, the square brackets now could be considered to be actual lists, rather than simply sequences of elements being passed for subsequent processing.
  6. Ultimately, the two-level map is assigned to the variable res which will have the type of Map<Version, Map<Level, List<String>>>.

Based on this information you can expect that the fragment


res
  .get(Version.JAVA_11)
  .get(Level.PROFESSIONAL) 

will return a List of two strings. Because the stream runs sequentially, the original order is maintained, so the list contains ["1Z0-816", "1Z0-819"]. From this you can deduce that the get(1) operation will return the value 1Z0-819. Therefore, the code runs and prints 1Z0-819 to the console.

Based on this, you can see that options A, B, D, and E are incorrect.

Conclusion: The correct answer is option C.

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.

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.

Share this Page