← Skill tree CS Skill Tree 0 CSCD211

Comparator.comparing by Key

Textbook: BJP (Reges and Stepp)

Before You Start

Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.

Not sure? Take the 60-second self-check.

1. Key extractor. What does Person::getLastName do when used as a Function<Person, String>?

Check

It is an unbound instance method reference. When applied to a Person object p, it returns p.getLastName(). As a Function<Person, String>, it maps each Person to a String.

2. Natural order. Comparator.comparing(Person::getLastName) compares Person objects by last name. What ordering does it use for the String keys?

Check

The natural order of String, which is lexicographic (case-sensitive, based on Unicode values). This is equivalent to calling String.compareTo.

3. Comparable constraint. Comparator.comparing(Person::getCity) compiles only if getCity() returns a type that satisfies what?

Check

The return type must implement Comparable. Without Comparable, there is no natural ordering to delegate to and the method fails to compile.


Try This First

The lambda (a, b) -> a.getLastName().compareTo(b.getLastName()) sorts Person objects by last name. Before reading further, count how many times the getLastName method reference appears in this lambda. Then write what you think Comparator.comparing(Person::getLastName) looks like.

Check

The lambda mentions getLastName twice (once for each argument). Comparator.comparing(Person::getLastName) mentions it once. The factory extracts the key from each argument internally and delegates the comparison to the key’s natural order.


What You Need To Walk In With

The Insight: Comparator.comparing(keyExtractor) is function composition: to compare two objects, apply keyExtractor to each (the inner function), then compare the resulting keys (the outer comparison). The same structure as compare(f(a), f(b)) in mathematics, where f is the key extractor. This is why the method reference appears only once: the factory handles the symmetric application to both arguments.

If the factory still feels like magic after reading, trace one call manually: Comparator.comparing(Person::getLastName).compare(pA, pB) is exactly equivalent to pA.getLastName().compareTo(pB.getLastName()). The factory does both steps for you. Once that trace clicks, the pattern becomes the obvious choice for any single-field sort.

You can: produce a Comparator<T> from any Comparable field using one method reference, choose the two-argument overload when you need a non-default key order, reach for comparingInt instead when the key is a primitive int, and explain why the method reference appears only once compared to the equivalent lambda.


How It Works

The one-argument overload

static <T, U extends Comparable<? super U>> Comparator<T>
    comparing(Function<? super T, ? extends U> keyExtractor)

In plain terms: give it a function from T to some Comparable type U; it returns a Comparator<T> that:

  1. Applies the function to each of the two T arguments.
  2. Compares the resulting U values using their natural order (U.compareTo).

The canonical form with a method reference:

Comparator<Person> byLastName = Comparator.comparing(Person::getLastName);

This reads: “compare Persons by their last name, in last-name natural order.”

The equivalent long form (lambda, written out):

Comparator<Person> byLastName = (a, b) -> a.getLastName().compareTo(b.getLastName());

The factory form expresses the same computation in fewer tokens and with the method reference appearing only once.

The two-argument overload

When the key type does not implement Comparable, or when a non-natural key order is needed, supply a comparator for the keys:

// Case-insensitive last name order
Comparator<Person> byLastNameCI =
    Comparator.comparing(Person::getLastName, String.CASE_INSENSITIVE_ORDER);

The second argument is a Comparator<U> that replaces the default natural order for the key.

When to use comparingInt instead

Comparator.comparing triggers autoboxing when the key is a primitive int. For int keys, use Comparator.comparingInt:

// comparing: autoboxes int to Integer on every call (allocation overhead)
Comparator<Player> bad  = Comparator.comparing(Player::getGamesPlayed);

// comparingInt: no boxing (correct)
Comparator<Player> good = Comparator.comparingInt(Player::getGamesPlayed);

comparingInt, comparingDouble: Primitive-Friendly Variants covers comparingInt, comparingLong, and comparingDouble in full.


Worked Example: Predict Then Check

For a Player class with getLastName(), getTeam(), and getGamesPlayed() (int), write the Comparator.comparing form for each ordering, identifying which overload is appropriate:

(a) Ascending by team name, case-sensitive. (b) Ascending by team name, case-insensitive. (c) Ascending by games played (int field).

Reasoning

(a) getTeam() returns String, which implements Comparable. One-argument overload. Uses natural String order (case-sensitive).

(b) Same key extractor, but need case-insensitive key order. Two-argument overload, passing String.CASE_INSENSITIVE_ORDER.

(c) getGamesPlayed() returns int (primitive). Use comparingInt to avoid boxing.

Show answers
// (a) case-sensitive team order
Comparator<Player> a = Comparator.comparing(Player::getTeam);

// (b) case-insensitive team order
Comparator<Player> b = Comparator.comparing(Player::getTeam, String.CASE_INSENSITIVE_ORDER);

// (c) games played (int field, no boxing)
Comparator<Player> c = Comparator.comparingInt(Player::getGamesPlayed);

Quick check

Check your understanding

A developer writes Comparator.comparing(Player::getTeam) where getTeam() returns String. Which statement is true?

Tier 1 · BJP (Reges and Stepp), Ch 10


Common Misconceptions

Misconception 1: using Comparator.comparing on a non-Comparable key type

Wrong mental model: “Any method reference works as a key extractor.”

Why it breaks: The one-argument overload requires the key type to implement Comparable. If Person::getCity returns a City class that does not implement Comparable<City>, the compiler reports a type error such as “no instance(s) of type variable(s) U exist so that City conforms to Comparable.”

How to correct: Either (a) add implements Comparable<City> to City and provide compareTo, or (b) use the two-argument overload with an explicit comparator for City values.

Source: Java 25 Comparator.comparing javadoc.

Quick check

Check your understanding

A City class has no implements clause. What happens when you call Comparator.comparing(Person::getCity) using the one-argument overload?

Tier 2 · BJP (Reges and Stepp), Ch 10


Misconception 2: using Comparator.comparing for a primitive key without boxing concern

Wrong mental model:Comparator.comparing(Player::getGamesPlayed) works fine for int getGamesPlayed().”

Why it matters: The compiler autoboxes int to Integer on each key extraction. For a one-time sort this is negligible. For tight loops, frequent sorts, or large arrays, the repeated Integer allocation adds GC pressure with no benefit. Comparator.comparingInt avoids all boxing.

How to correct: Use Comparator.comparingInt(Player::getGamesPlayed) whenever the key is a primitive int. The API is identical; only the internal implementation differs (no boxing).

Source: Bloch, Effective Java, Item 6.


Formal Definition and Interface Contract

From the Java 25 Comparator API:

static <T, U extends Comparable<? super U>>
    Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor)

Accepts a function that extracts a Comparable sort key from a type T, and returns a Comparator<T> that compares by that sort key.

Two-argument overload:

static <T, U>
    Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor,
                            Comparator<? super U> keyComparator)

Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.


Mental Model

Think of Comparator.comparing(keyExtractor) as building a pipeline. Given two objects a and b, the pipeline runs: keyExtractor(a) on one side and keyExtractor(b) on the other, then compares the outputs with the key’s natural order. The key extractor is the inner function; the natural order comparison is the outer function. Comparator.comparing wires the two together without the caller having to write both applications manually.

This is compare(f(a), f(b)) in mathematical notation, where f = keyExtractor and compare is the key’s natural order.


Connections

Within CSCD 210/211: Instance Method Reference on the First Argument introduced Person::getLastName as an unbound instance method reference. This lesson shows its primary production use: as the key extractor argument to Comparator.comparing.

Looking back: The named-class comparators covered in Implementing Comparator<T> as a Named Class (like TeamComparator) can be replaced with Comparator.comparing(Player::getTeam). This lesson is the modern replacement for any single-field named-class comparator on a String or Comparable field.

Looking ahead: reversed, nullsFirst, and nullsLast covers reversed() and thenComparing(), which chain Comparator.comparing calls into multi-key comparators. comparingInt, comparingDouble: Primitive-Friendly Variants covers comparingInt, comparingLong, and comparingDouble.


Go Deeper (optional)

None of what follows is needed to write correct, idiomatic sorting code. It is here for readers who enjoy knowing where an idea comes from.

Where this idea sits in computer science. A Comparator is a binary relation that imposes a total order on its type: the contract rules (antisymmetry, transitivity, totality) are the axioms of a strict total order from order theory. Comparator.comparing then builds a derived total order from an existing one by pulling values through a function. In type theory, this is a covariant transformation: if U has an order, then any function f : T -> U induces an order on T defined by a <= b when f(a) <= f(b).

The design-pattern angle. The function-object passed to Comparator.comparing is the Strategy pattern in its simplest possible form: a single-method object that varies one piece of an algorithm (the key extraction) while the surrounding logic (extract, compare, return sign) stays fixed. This is also the most direct demonstration of a first-class function in Java: instead of naming a subclass, you pass the behavior as data.

The career-relevance angle. In professional Java codebases, Comparator.comparing is the expected form for any single-field sort. Code reviews routinely flag the lambda form (a, b) -> a.getName().compareTo(b.getName()) and suggest replacing it with Comparator.comparing(T::getName). Knowing this pattern and its two-argument overload covers the vast majority of sorting situations you will encounter on the job or in a technical interview.

Going further inside this API. Chaining Comparator.comparing(Player::getTeam).thenComparing(Comparator.comparingInt(Player::getGamesPlayed)) produces a fully declarative multi-key comparator with no explicit comparison logic at the call site. That is covered in reversed, nullsFirst, and nullsLast. The resulting chain reads almost like a sentence, which is the point: the goal of the API design was to make sort specifications as close to spoken English as a statically typed language allows.


Practice

Level 1

Write three comparators using Comparator.comparing:

(a) Comparator<Book> ordering by getTitle() (String), ascending.

(b) Comparator<Book> ordering by getTitle(), case-insensitive.

(c) Comparator<Book> ordering by getPageCount() (int), ascending.

Show answer
// (a)
Comparator<Book> byTitle = Comparator.comparing(Book::getTitle);

// (b)
Comparator<Book> byTitleCI = Comparator.comparing(Book::getTitle, String.CASE_INSENSITIVE_ORDER);

// (c)
Comparator<Book> byPages = Comparator.comparingInt(Book::getPageCount);

Level 2

Convert the following named comparator class to a single Comparator.comparing expression:

public class PlayerByTeamComparator implements Comparator<Player>
{
    @Override
    public int compare(final Player a, final Player b)
    {
        if (a == null || b == null)
            throw new IllegalArgumentException("Bad Player in PlayerByTeamComparator.compare");
        return a.getTeam().compareTo(b.getTeam());
    }
}
Show answer
Comparator<Player> byTeam = Comparator.comparing(Player::getTeam);

The null check is no longer needed at the call level: if Player objects passed to compare are null, the getTeam() call will throw NullPointerException from the extractor; that is acceptable for a non-null-tolerant comparator. For null-tolerant behavior, wrap: Comparator.nullsLast(Comparator.comparing(Player::getTeam)).


Level 3

Explain in 3-4 sentences why Comparator.comparing(Player::getTeam) has a structural connection to mathematical function composition. Use the notation compare(f(a), f(b)) in your explanation.

Show answer

Comparator.comparing(keyExtractor) builds a comparator that, given two Player objects a and b, computes compare(f(a), f(b)), where f = Player::getTeam (the key extractor) and compare is String’s natural order. The key extractor f is the inner function: it maps each Player to a String. The outer operation compares the two extracted strings. This is exactly function composition in mathematics: the comparison function is composed with the key extractor applied simultaneously to both arguments. The two-argument overload Comparator.comparing(f, keyComparator) replaces the natural-order comparison with any Comparator<U>, giving keyComparator.compare(f(a), f(b)), which is the full composition.


Check Yourself

Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.

Check your understanding

Which statement correctly describes what Comparator.comparing(Person::getLastName) returns?

Tier 1 · BJP (Reges and Stepp), Ch 10

The one-argument overload Comparator.comparing(keyExtractor) requires that the key extractor’s return type satisfies which constraint?

Tier 1 · BJP (Reges and Stepp), Ch 10

A Player class has int getGamesPlayed(). Which call is preferred and why?

Tier 2 · BJP (Reges and Stepp), Ch 10

Trace this code and predict the output. import java.util.*; class Book { private final String title; Book(String t) { title = t; } public String getTitle() { return title; } public String toString() { return title; } } List<Book> books = new ArrayList<>(); books.add(new Book(“Zebra”)); books.add(new Book(“apple”)); books.add(new Book(“Mango”)); books.sort(Comparator.comparing(Book::getTitle)); System.out.println(books);

Tier 2 · BJP (Reges and Stepp), Ch 10

A City class does not implement Comparable. Which call compiles correctly to sort Person objects by their getCity() field?

Tier 3 · BJP (Reges and Stepp), Ch 10