Lesson I9: Case Study#
In this lesson, we will examine a default method in the Comparator interface. The implementation and syntax is overwhelming. We will start off by exploring how the String class can have a sequence of methods chained together. We’ll then introduce some functional programming into the example. Lastly, we’ll extend beyond the String class to the Comparator. By the end of this lesson, you should:
Have the skills necessary to develop understanding of complicated code.
Be able to explain the
thenComparingmethod.
The Goal
The goal is to understand this terse code. It might look relatively simple, but it can be hard to explain. Furthermore, the thenComparing implementation looks quite complex. We aim to understand all of it.
// Compose a comparator using primary, secondary and tertiary comparisons
Comparator<Integer> primary = (i1, i2) -> i1 % 2 - i2 % 2;
Comparator<Integer> comparator = primary
.thenComparing((i1, i2) -> i1 % 10 - i2 % 10)
.thenComparing(Integer::compare);
// now sort with our composed comparator
list.sort(comparator);
Method Chaining#
Consider the following code.
// one line at a time
String str = "example";
str = str.substring(0, 4);
str = str.toUpperCase();
// chained together
String str = "example".substring(0, 4).toUpperCase();
Because substring returns a String, we can immediately call another method on the returned object on the same line. This technique is called method chaining. APIs designed to support this style are said to provide a Fluent Interface.[1]
The purpose of the above code is to illustrate that many of the String methods return a String object which can be directly referenced on the same line of code. This shortens the code and can often make things more readable.
Adding Functional Programming#
The above String code is pretty simple. Let’s add some more method calls. The following code is not practical; it is used for illustration purposes only.
In the first method noChain, the code will convert a string to upper case, then invoke the method removeMirror, and then replaces all "A" with "X". Lastly, it takes only the first 3 characters of the string if the string is long enough.
The second implementation chainEm does exactly the same thing in one statement. For readability, each link in the chain is put on its own line. This is common practice. Since each method returns a string, we can continue to use the dot operator to create a chain.
public static void noChain(String str) {
str = str.toUpperCase();
str = removeMirror(str);
str = str.replace("A", "X");
str = str.substring(0, Math.min(3, str.length()));
}
public static void chainEm(String str) {
str = str.toUpperCase()
.transform(EggsAmple1::removeMirror)
.replace("A", "X")
.transform(s -> s.substring(0, Math.min(3, s.length())));
}
The transform method has a single argument, a Function<> interface. In our example, the interface is a functional interface that is fulfilled with a method that takes a String and returns a String. We give it a method pointer to the static method implemented in the EggsAmple1 class. Then we chain some more.
The last call to transform uses a Lambda Expression to fulfill the interface. This isn’t necessary and it is overkill. We do it simply to illustrate that the argument to transform is an interface that can fulfilled in more than one way.
The transform method is what introduces functional programming because its argument is a function.
Implementing the Comparator#
Let’s start off by attempting to implement the thenComparing method. Then we’ll compare our implementation to the actual code in the Comparator interface.
We want to support chaining, meaning that we want to return a Comparator in the same way that the String methods above returned a String.
We are first going to simplify our problem by treating Comparator as a class that works only with Integers. Then we’ll convert it to an interface, and then to a generic interface.
1// implemented as a class
2public class Comparator {
3 public int compare(Integer i1, i2) { return i1 - i2; }
4
5 public Comparator thenComparing(Comparator secondary) {
6 // Use primary comparison. i1 and i2 are not defined. This won't work!
7 int res = this.compare(i1, i2);
8 if (res == 0) {
9 // use secondary comparison
10 res = secondary.compare(i1, i2);
11 }
12 return res;
13 }
The above code won’t work for two reasons:
In line 7 we need two integer values, and we have no way to get them!
On line 12, we are returning an
inttype, but the method prototype expects us to return a Comparator!
Let’s ignore this for a second to first understand the attempted functionality.
The code first compares two Integers. If they are the same, it proceeds to to compare them using a secondary comparison implementation.
Attempt #1 to Fix
In order to get line 7 to work, we need to get access to i1 and i2. To illustrate how this is done, we will make up some syntax that is invalid in hopes that it acts like a bridge to the final solution. Let’s alter the code as follows:
Invalid Code
The following code is bad. Java does not allow one to declare a method inside another method like this.
1// implemented as a class
2public class Comparator {
3 public int compare(Integer i1, i2) { return i1 - i2; }
4
5 public Comparator thenComparing(Comparator secondary) {
6 public int composedCompare(Integer i1, Integer i2) {
7 int res = this.compare(i1, i2);
8 if (res == 0) {
9 // use secondary comparison
10 res = secondary.compare(i1, i2);
11 }
12 return res;
13 }
14 return new Comparator(composedCompare);
15 }
16 public Comparator (CompareMethod method) { }
It is obvious that we now have access to i1 and i2 because they are arguments to our nested method composedCompare.
We have now also created a new Comparator object to return. We imagined a constructor that takes a method to be used as its compare method instead of the one coded on line 3. While this syntax does not work, the idea is sound. We created and returned an object with an altered version of the compare method.
How do we fix the syntax?
Attempt #2 to Fix
To fix the syntax, we need to use interfaces.
1public interface Comparator {
2 // abstract method provided the by implementer
3 public int compare(Integer i1, Integer i2);
4
5 default Comparator thenComparing(Comparator secondary) {
6 final Comparator primary = this;
7 // create a Comparator using Anonymous Inner Class syntax
8 Comparator composed = new Comparator() {
9 @Override
10 public int compare(Integer i1, Integer i2) {
11 int res = primary.compare(i1, i2);
12 if (res == 0) {
13 // use secondary comparison
14 res = secondary.compare(i1, i2);
15 }
16 return res;
17 }
18 };
19 return composed;
20 }
21}
The above code will now compile!! We had to introduce the default keyword to the method thenComparing. We chose to use an Anonymous Inner Class syntax to create an instance of a Comparator with the abstract method implemented.
Attempt #3: Using Lambdas and Generics
Let’s now rewrite the code another time using a Lambda Expression and as a Generic.
public interface Comparator<T> {
// abstract method provided the by implementer
public int compare(T i1, T i2);
default Comparator<T> thenComparing(Comparator<T> secondary) {
// create a Comparator using a lambda
Comparator<T> composed = (i1, i2) -> {
int res = this.compare(i1, i2);
if (res == 0) {
// use secondary comparison
res = secondary.compare(i1, i2);
}
return res;
};
return composed;
}
}
We added the <T> so that it will work with any type, not just Integers. The code became a little shorter using the lambda expression. We also eliminated the identifier primary and, instead, just referenced this directly. This implementation is very close Java’s implementation.
In the Java final implementation, it:
Uses Constraint Typing to make the Generic typing more extensive (inclusive).
Checks that the argument passed in is not null.
Directly returns the lambda expression, avoiding the creation of a local variable.
Explicitly casts the lambda expression so that also implements
Serializable.[3]Uses a ternary operator to eliminate the if-statement, making the code even shorter.
public interface Comparator<T> {
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
Decomposing the Comparator#
Let’s look at an even more complex implementation of thenComparing. In this implementation, the idea is very similar to the above. We want to allow chaining of Comparators. However, we want to sort Objects by some field. Java accomplishes this in two steps: first it overloads thenComparing with an implementation that calls a helper method comparing. Let’s look a the unsimplified code first.
public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator) {
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}
default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator) {
return thenComparing(comparing(keyExtractor, keyComparator));
}
The primary source of confusing comes from all the Generic Typing. So let’s simplify that first by specifying the Generics and adding some comments. Below we have modified the code in a number of ways:
Replace T with
DogReplace U with
Bark(a field or “key” of the Dog class)Rename
keyExtractortogetsBarkRename
keyComparertocomparesBarksDelete validation code (checking for null)
Separating the return and lambda lines
// this will get a comparator that compares two Dogs by their Bark.
// It needs two helpers:
// 1) A method to get the Bark from the Dog (keyExtractor)
// 2) A Comparator that knows how to compare Barks (keyComparator)
public static Comparator<Dog> comparing(
Function<Dog, Bark> getsBark, // a function that gets the Bark from the Dog
Comparator<Bark> comparesBarks) { // a Comparator that compares Barks
// to compare Dogs, actually compare their barks. Both Comparators return `int`
Comparator<Dog> res = (d1, d2) -> comparesBarks
.compare(getsBark.apply(d1), getsBark.apply(d2));
return res;
}
// This is an overload of thenComparing
default <Bark> Comparator<Dog> thenComparing(
Function<Dog, Bark> getsBark,
Comparator<Bark> comparesBarks) {
// call static method to get our Dog Comparator
Comparator<Dog> dogComparator = comparing(getsBark, comparesBarks);
// Call our earlier implementation of thenComparing that
// takes a single Comparator Argument
return thenComparing(dogComparator);
}
Note that getsBark is of type Function<T, R>. This means that it is an interface and we must invoke the abstract method apply.
Look at the revision. Is it easier to read? I hope so!
Can you now explain the details of what is happening in the following code?
// Compose a comparator using primary, secondary and tertiary comparisons
Comparator<Integer> primary = (i1, i2) -> i1 % 2 - i2 % 2;
Comparator<Integer> comparator = primary
.thenComparing((i1, i2) -> i1 % 10 - i2 % 10)
.thenComparing(Integer::compare);
// now sort with our composed comparator
list.sort(comparator);
What’s so important?
#
Here is how you can simplify code so that you better understand it:
Because generics make code hard to read, first eliminate all the
extendsandsuperBreak up single lines into multiple lines (e.g. temporary assignment then return)
Rename Generics to something common (e.g. Dog and Bark)
This requires an understanding of their relationship, which can be hard.
Consider eliminating the Generic altogether and hard-coding to Integer
Footnotes#
[1] Fluent Interface
Fluent interface is the practice of designing APIs so that method calls can be chained together, usually by having methods return the current object (or this, or another suitable object), resulting in code that reads naturally and expresses intent clearly.
// In this example, each `set` method returns `this`
Person p = new Person()
.setName("Alex")
.setAge(21)
.setCity("Seattle");
[2] Ternary Operator
A ternary operator is a compact conditional expression that chooses between two values based on a boolean condition. In Java, it has the form:
// generic form
Result r = condition ? valueIfTrue : valueIfFalse;
// example
int max = (a > b) ? a : b;
The condition is evaluated first; if it is true, the expression yields the first value, otherwise it yields the second. Because the ternary operator is an expression (not a statement), it produces a value and can be used directly in assignments or method arguments.
It is often used as a concise alternative to a simple if–else statement. While ternary expressions can make code shorter and clearer for simple decisions, they should be used sparingly. Overly complex or nested ternary expressions can reduce readability.
[3] Serializable
When you see a lambda cast as (Comparator<T> & Serializable), Java is being told that the same lambda object should implement more than one interface. By default, a lambda only implements a single functional interface—in this case, Comparator<T>. Adding & Serializable explicitly marks the lambda as also implementing Serializable, which means the resulting object can be serialized (converted into a byte stream for storage or transmission).
Serializable is a marker interface in Java (it has no methods) that signals to the JVM and libraries that an object is allowed to be serialized—for example, written to a file, cached, or sent over a network. Lambdas are not serializable by default, even if the functional interface is, so this intersection cast is required when a framework or API expects a serializable object. In short, (Comparator<T> & Serializable) ensures the lambda can be used in contexts that require both comparison behavior and safe persistence or transport.