OCP Question 11, Explanation

Given:

class Dog {
    String name;
    String breed;
    public Dog (String name, String breed) {
        this.name = name;
        this.breed = breed;
    }
    public String getName() { return name; }
    public String getBreed() { return breed; }
}

and the code fragment:

List<Dog> kennels = Arrays.asList (
    new Dog ("Oliver", "Collie"),
    new Dog ("Sam", "Beagle"),
    new Dog ("Jack", "Beagle"),
kennels.stream()
// line n1
       .collect(Collectors.toList());

Which code fragment, when inserted at line n1, sorts the list of dogs in descending order of breed and then ascending order of name?

A. .sorted(Comparator.comparing(Dog::getBreed).reversed().thenComparing(Dog::getName))
B. .sorted(Comparator.comparing(Dog::getBreed).thenComparing(Dog::getName))
C. .map(Dog::getBreed).sorted(Comparator.reverseOrder())
D. .map(Dog::getBreed).sorted(Comparator.reverseOrder().map(Dog::getName).reversed())

 

The correct answer is A.

Brief explanation:

Comparator.comparing(Dog::getBreed) returns Comparator<String> (because the metref Dog::getBreed returns breed, which IS-A String), reversed() is used to flip this Comparator’s order (because String’s natural order is ascending but we need descending), and thenComparing() applies name-based lexicographic, that is, ascending order to the Dog objects whose breed fields have identical values. The resulting two-tier Comparator is finally passed to sorted().

 

 

Full explanation
The code invokes the stream() method on kennels, whose reftype is List<Dog>, so now we have a Stream of Dog. (EXTRA: Note in passing that the List interface doesn’t define stream(), it inherits this method from the Collection interface.)

Okay, so our task is to sort dogs by breed from Z to A, and when they are of the exact same breed, we must arrange such dogs by their name from A to Z. Accordingly, the final list must contain dogs in this order: Oliver, Jack, Sam.

Let’s make this question executable to see if our conclusions are correct and also to have fun by playing with the code:

package host.igor;
import java.util.List;
import java.util.Arrays;
import java.util.Comparator;
import java.util.stream.Collectors;

public class Dog {
    String name;
    String breed;
    public Dog (String name, String breed) {
        this.name = name;
        this.breed = breed;
    }
    public String getName() { return name; }
    public String getBreed() { return breed; }
    @Override
    public String toString() { return this.name + "-" + this.breed; }
}
class Test{
    public static void main(String[] args) {
    List<Dog> kennels = Arrays.asList (
        new Dog ("Oliver", "Collie"),
        new Dog ("Sam", "Beagle"),
        new Dog ("Jack", "Beagle"));
    kennels.stream()
           .sorted(Comparator.comparing(Dog::getBreed).reversed().thenComparing(Dog::getName))    // option A
//           .sorted(Comparator.comparing(Dog::getBreed).thenComparing(Dog::getName))             // option B
//           .map(Dog::getBreed).sorted(Comparator.reverseOrder())                                // option C
//           .map(Dog::getBreed).sorted(Comparator.reverseOrder().map(Dog::getName).reversed())   // option D
           .collect(Collectors.toList())
           .forEach(System.out::println);    // Oliver-Collie \n Jack-Beagle \n Sam-Beagle
    }
}

Now run the Test class; as we can see, the very first option is indeed the correct one.

We added the toString() method to the Dog class to describe dogs, and the forEach() method to Test to actually view their names and breeds. Yes, collect() is a terminal operation and forEach() after it may look odd at first but then you realize that this forEach() belongs in fact to List who inherited it from Iterable.

It is also possible to use peek() instead of forEach(), or simply assign the collect()’s returned value to an appropriately typed var and then print it. Please do this on your own (click here for suggested solutions for these and other additional exercises to the current Problem).

Ordinarily, streams are sorted with the help of the sorted() method defined in the Stream interface. I say ‛ordinarily’ because options C and D try to apply map() instead of sorted(), which is rather unusual, requires jumping through extra hoops but theoretically possible. And while we are at this point, please tell me what you think of the following slide (used in Brian Goetz’s talk “Java 8 Streams: Lambda in Top Gear ” at JavaOne 2013, https://www.youtube.com/watch?v=UKuFqAhDEN4&t=630; jump to ~10:30 if playback starts @ 00:00):

Click here for my opinion.

Alright, back to our Problem. Stream has two overloaded sorted() methods:

Let’s play with the zero-arg sorted() first:

Function<Dog, String> mapB = x -> x.getBreed();   // as a lambda
Function<Dog, String> mapN = Dog::getName;        // almost same but in metref form
Consumer<String> cons = System.out::println;      // because I’m lazy :)

kennels.stream()
       .map(mapB)
       .sorted()
       .forEach(cons);                            // Beagle \n Beagle \n Collie

We defined three more variables simply to save time on typing by recycling those vars in other scenarios.

What’s the story with map() before sorted()? Well, it’s needed because Dog doesn’t define its natural order. Say, running

kennels.stream()
       .sorted()
       .forEach(System.out::println);             // or simply re-type Consumer to Dog

results in ClassCastException: “host.igor.Dog cannot be cast to java.lang.Comparable“. Which is expected since Dog doesn’t implement Comparable, and that’s why sort() has no idea how to compare two Dog objects. Okay, let’s fix this (changes are in red):

public class Dog implements Comparable<Dog>{
    String name;
    String breed;
    public Dog (String name, String breed) {
        this.name = name;
        this.breed = breed;
    }
   @Override
   public int compareTo(Dog obj){ return this.name.compareTo(obj.name); }
   
   public String getName() { return name; }
   public String getBreed() { return breed; }
   public void getDog() { System.out.println(name + "-" + breed); }
   @Override
   public String toString() { return this.name + "-" + this.breed; }
}
class Test{
    public static void main(String[] args) {
    List<Dog> kennels = Arrays.asList (
        new Dog ("Oliver", "Collie"),
        new Dog ("Sam", "Beagle"),
        new Dog ("Jack", "Beagle"));
    Function<Dog, String> mapB = x -> x.getBreed();
    Function<Dog, String> mapN = Dog::getName;
    Consumer<Dog> cons = System.out::println;
    kennels.stream()
//           .map(mapB)
           .sorted()
           .forEach(System.out::println);         // Jack-Beagle \n Oliver-Collie \n Sam-Beagle
    }
}

The above scenario sorts dogs by their name, which IS-A String, that’s why the order is ascending. How would you go about sorting COMPARABLE dogs by their breed in descending order? (click here for suggested solution).

Clearly, this is too much work, and the Stream interface helpfully provides a better way through its overloaded sorted(Comparator comp) method, so let’s create a suitable Comparator, which would define reversed order.

The Comparator interface has two overloaded comparing() methods; we’ll be needing the single-arg one because the second version is only useful when we want to override default behavior of the extracted comparison key. And what is comparison key in our case? it’s a String, of course! because we’ll be comparing the values of breed and name.

Okay, so now we require a Function object that extracts String values from Dog. But look: we’ve already defined two Functions – mapB and mapN – which are doing exactly what we need (don’t forget to roll back and make dogs INCOMPARABLE first):

Function<Dog, String> mapB = x -> x.getBreed();
Function<Dog, String> mapN = Dog::getName;
Consumer<Dog> cons = System.out::println;

kennels.stream()
       .sorted(Comparator.comparing(mapB))
       .forEach(cons);       // Sam-Beagle \n Jack-Beagle \n Oliver-Collie

Ah, this works as advertised but the order is ascending (Beagle, Beagle, Collie). We flip it by invoking handy reversed() method

on the Comparator returned by comparing():

kennels.stream()
       .sorted(Comparator.comparing(mapB).reversed())
       .forEach(cons);       // Oliver-Collie \n Sam-Beagle \n Jack-Beagle

Outstanding! The dogs are finally listed by their breed in descending order… But we’ve got one more thing to do, which is make sure dogs of the same breed are arranged by their name in ascending order. For such cases the Comparator interface provides a whole bunch of thenComparing() methods, of which we’ll take just the second one (simply because it was mentioned by our Problem):

And look, we’ve already got suitable Function for it! It’s mapN:

kennels.stream()
       .sorted(Comparator.comparing(mapB).reversed().thenComparing(mapN))
       .forEach(cons);       // Oliver-Collie \n Jack-Beagle \n Sam-Beagle

Bingo! This is option A in disguise, just replace those mappers and the Consumer with their verbose equivalents. All in all, we’ve managed to sort our dogs by breed descending (Collie, Beagle, Beagle) and after discovering two beagles we list them by their name ascending (Jack followed by Sam).

By the way, would it be possible to use the first version of thenComparing()? Click here after answering.

Let’s make absolutely sure you understand when and how thenComparing() is used: it gets applied IF AND ONLY IF the original comparator cannot differentiate between two objects.

Illustration:

class Dog{ /* remains unchanged */   }

class Test{
    public static void main(String[] args) {
    List<Dog> kennels = Arrays.asList (
      new Dog ("Oliver", "Collie"),
      new Dog ("Sam", "Beagle"),
//       new Dog ("Jack", "Beagle"),           // uncommenting this LOC makes INVOKED printed 4 times
      new Dog ("Jack", "Bulldog"));
    Function<Dog, String> mapB = x -> x.getBreed();
    Function<Dog, String> mapN = Dog::getName;
    Consumer<Dog> cons = System.out::println;
    kennels.stream()
           .sorted(Comparator.comparing(mapB)
                             .reversed()
                             .thenComparing(x -> {
                                 System.out.println("INVOKED");
                                 return x.getName(); }))
           .forEach(cons);
    }
}

After finding answer to the current question on the real exam we move immediately to the next question to save time. At the Review stage, however, we’ll have to see why options B through D are indeed incorrect. Here they are:

B. .sorted(Comparator.comparing(Dog::getBreed).thenComparing(Dog::getName))
C. .map(Dog::getBreed).sorted(Comparator.reverseOrder())
D. .map(Dog::getBreed).sorted(Comparator.reverseOrder().map(Dog::getName).reversed())

It should be clear by this minute that B is out because it didn’t flipped first Comparator’s order from A-Z to Z-A, so breeds get listed ascending.

Option C is also syntactically valid but it doesn’t even look at names and merely reverses breeds by flipping String’s natural order (ref.to reverseOrder() javadoc).

As for option D, it throws a comperr. In my eyes, D is faulty because Comparator.reverseOrder() returns a Comparator, and then map(Dog::getName) gets invoked on this object – but there’s no map() in Comparator! Unfortunately, the compiler complains of something entirely different:  ‛non-static method getName cannot be referenced from static context’. Oh really? Then how about this one?

kennels.stream()
       .map(Dog::getBreed).sorted(Comparator.<String>reverseOrder()
                                            // .map(Dog::getName)
                                            .reversed())
       .forEach(cons);     // Beagle \n Beagle \n Collie

See? No comperr even though reversed() is an instance method, too! Naturally, we needed first to type to String the Comparator returned by reverseOrder() but it works!

Or simply make getName() and name static and see what I mean…

Takeaway: We sort streams by applying sorted(); sorted(_no_args_) uses natural order, while sorted(Comparator comp) needs a Comparator, which we create by using various static and default methods from the Comparator interface. Comparator.comparing() returns a Comparator that compares objects based on a certain criterion called ‛key’ (usually it’s a field of type String, or int, etc.; something that can describe the state of the compared objects; most likely to appear in the overridden equals()). It is also possible to flip the behavior of this Comparator by invoking on it the reversed() method. The Comparator.reverseOrder() method is used when the objects in question do have their natural order defined. The thenComparing() method lets add one more criterion for comparison if initial Comparator cannot distinguish between two objects.

Leave Comment

Your email address will not be published.