← Skill tree CS Skill Tree 0 CSCD211

When Comparable Is Right and When a Comparator Is Right

Textbook: BJP (Reges and Stepp)

Quick links: The one question | Decision table | Both at once | Misconceptions | Practice

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. What is the return contract for compareTo? What do negative, zero, and positive mean?

Check

a.compareTo(b) returns a negative integer when a comes before b, zero when they are equivalent, and a positive integer when a comes after b. The magnitude beyond sign is not specified by the contract. See the Comparable javadoc (Java 25).

2. What is the difference in how you call Arrays.sort when you have a Comparable type versus when you have a Comparator?

Check

Arrays.sort(arr) (one argument) uses the elements’ own compareTo. Arrays.sort(arr, comparator) (two arguments) uses the provided Comparator and ignores compareTo entirely. If the type does not implement Comparable and you call the one-argument form, you get a ClassCastException at runtime.

3. Can a class implement Comparable<T> more than once, say once for T = Receipt and once for T = Timestamped?

Check

No. Java generics erase to raw types at runtime, so implements Comparable<Receipt> and implements Comparable<Timestamped> would both erase to Comparable and collide. A class can implement Comparable<T> for at most one type T. There is no such limit on Comparator objects: you can write as many as you need. (Source: Bloch.EJ3.Item-14.)


The One Question

Before you write a single line of comparison code, answer this question out loud:

“If a colleague reads Arrays.sort(items) with no second argument, can they predict the result without looking at the source?”

Predict your answer for a list of Receipt objects sorted by the call Arrays.sort(receipts). What order would you expect?

Why that question matters

If you can predict the result, there is a default ordering worth baking into the class. If your first instinct was “sort by what?” the class probably should not implement Comparable at all. The question forces you to think from the caller’s perspective rather than the implementer’s convenience, which is the right frame for this decision.


What You Need To Walk In With

Comparable defines the one natural order baked into the class; a Comparator is a separate, swappable ordering supplied from outside. The distinction is not about complexity: it is about ownership. Natural order belongs to the type. Every other ordering belongs to the context that needs it.

Once you have worked through this material, you can: apply the one-question test to any new type and defend the answer in a sentence, recognize the compareToByX-method anti-pattern and rewrite it with named Comparator constants, predict the silent failure that occurs when compareTo reads a mutable field and the object is stored in a TreeSet, and design the both/and pattern with public static final Comparator constants placed inside the owning class.


How It Works

Two kinds of ordering, two places to put them

Java provides two interfaces for comparison:

Interface Who owns the ordering Where it lives Used by
Comparable<T> The type itself Inside the class, in compareTo Arrays.sort(arr), TreeSet, TreeMap (no second argument)
Comparator<T> The caller or a utility class Outside the class Arrays.sort(arr, cmp), TreeSet(cmp)

The choice of which to use is a design decision, not a technical one. Both work. The question is which produces code that reads correctly and surprises no one.

The natural-order test

A type has a natural order when one ordering is so obviously correct that every reasonable client would choose it unprompted. Examples:

A type does not have a natural order when “in what order?” is a question that depends on who is asking:

For types without a natural order, implement no Comparable at all. Provide named Comparator objects instead.

Canonical code skeleton

The following Player design appears in the F19 Lab 1 review materials and is the template for the both/and pattern:

// Player has a natural order (by name) AND alternate Comparators.
public class Player implements Comparable<Player> {

    private final String name;
    private final String team;
    private final int gamesPlayed;

    public Player(String name, String team, int gamesPlayed) {
        this.name = name;
        this.team = team;
        this.gamesPlayed = gamesPlayed;
    }

    // Natural order: alphabetical by name.
    // A colleague reading Arrays.sort(roster) can predict this without
    // looking at the source, so it belongs in compareTo.
    @Override
    public int compareTo(final Player other) {
        return this.name.compareTo(other.name);
    }

    // Alternate ordering 1: by team, then by name within a team.
    public static final Comparator<Player> BY_TEAM =
        Comparator.comparing((Player p) -> p.team)
                  .thenComparing(p -> p.name);

    // Alternate ordering 2: most games played first.
    public static final Comparator<Player> BY_GAMES_PLAYED_DESC =
        Comparator.comparingInt((Player p) -> p.gamesPlayed).reversed();
}
// Callers at the sort site:
Arrays.sort(roster);                          // natural order: by name
Arrays.sort(roster, Player.BY_TEAM);          // by team
Arrays.sort(roster, Player.BY_GAMES_PLAYED_DESC); // most games first

Notice that compareTo handles exactly one ordering. Every other ordering is a named Comparator constant. There are no compareToByTeam methods cluttering the class.

Decision table

Situation Use
One obvious default order; callers agree without prompting Comparable (+ Comparators for the rest)
Multiple orderings, none more natural than another Comparators only; no Comparable
Third-party type you cannot modify Comparator only (you cannot add Comparable)
One-off sort inside a single method Lambda passed to Arrays.sort or List.sort
Type already has a natural order, and you need an extra one Add a Comparator; leave compareTo untouched

Quick check

Check your understanding

A Receipt class is used for a daily ledger (chronological), a biggest-sales report (by amount), and a customer export (by ID). Which design does the lesson recommend?

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

Worked Example and Algorithmic Steps

Predicting then checking: the Receipt design

You are designing a Receipt class for a point-of-sale system. The system sorts receipts in three contexts:

  1. By timestamp (ascending) for the daily ledger.
  2. By total amount (descending) for the “biggest sales” report.
  3. By customer ID for export.

Predict: Which of the following designs is correct?

Thought process

Work through each option:

  • Option A: A compareTo that behaves differently depending on a flag breaks the contract. The result of compareTo must be consistent over the life of the objects (required for TreeSet correctness). A flag makes it non-deterministic. Reject.
  • Option B: The daily ledger is the system’s primary view, so timestamp-ascending is the obvious default. A colleague reading Arrays.sort(receipts) would reasonably guess “chronological order.” That default belongs in compareTo. The two alternate orderings are context-specific and go into named Comparators. This is correct.
  • Option C: Valid if there truly is no natural default. Here there is one (timestamp for the ledger), so this gives up information without benefit.
  • Option D: Impossible. A class can implement Comparable<T> for at most one T (source: Bloch.EJ3.Item-14).
Show answer

Correct answer: B.

Timestamp is the natural order because the daily ledger is the system’s primary view. The other two orderings are context-specific and belong in named Comparators. Option D is impossible: a class can implement Comparable<T> only once.

public class Receipt implements Comparable<Receipt> {

    private final long timestampEpochMs;
    private final int  totalCents;
    private final String customerId;

    // Natural order: chronological (primary view = daily ledger).
    @Override
    public int compareTo(final Receipt other) {
        return Long.compare(this.timestampEpochMs, other.timestampEpochMs);
    }

    public static final Comparator<Receipt> BY_AMOUNT_DESC =
        Comparator.comparingInt((Receipt r) -> r.totalCents).reversed();

    public static final Comparator<Receipt> BY_CUSTOMER_ID =
        Comparator.comparing((Receipt r) -> r.customerId);
}

Step-by-step decision procedure

  1. Name the type and list every ordering the system needs.
  2. Ask: “Is one of these orderings so obvious that no caller would be surprised by Arrays.sort(items)?” If yes, that ordering is the natural order.
  3. Put the natural order in compareTo. If none exists, implement no Comparable.
  4. Write one named Comparator per remaining ordering. Place them as public static final constants inside the class (or in a companion utility class for large families).
  5. Verify that the field(s) read by compareTo are immutable (final, set in the constructor, never mutated). If they are mutable, reconsider the design.

Common Misconceptions

Misconception: force Comparable onto every class just to be safe

Wrong mental model: “Every class should implement Comparable. Pick some ordering and add it; it cannot hurt.”

Why it breaks on a small example: Write a Color class with red, green, blue fields. Now ask three colleagues independently what Arrays.sort(palette) should produce. If they give different answers (by brightness? by red channel? by hue?), there is no natural order. Adding any compareTo would surprise at least two of them.

How to correct: Provide named Comparators (Color.BY_RED, Color.BY_BRIGHTNESS) instead. Force the caller to pick explicitly. The compiler-enforced second argument to Arrays.sort communicates intent at the call site in a way that an invisible compareTo never can.

Source: Bloch.EJ3.Item-14 (the canonical “when to implement Comparable” discussion); pitfalls.md practitioner observation.

Quick check

Check your understanding

A developer adds compareTo to a Color class (with red, green, blue fields) to sort by red channel, saying it cannot hurt to have an ordering. What does the lesson say is wrong with this reasoning?

Tier 2 · Bloch.EJ3.Item-14

Misconception: natural order is whichever ordering is easiest to implement

Wrong mental model: “My Player has a String name field, and String.compareTo is right there, so I will sort by name.”

Why it breaks on a small example: If the system’s primary view is the standings table (sort by win percentage, then by team name), a name-alphabetical compareTo produces wrong output when someone calls Arrays.sort(roster) without thinking. The ease of implementation becomes a trap: the “natural” order is an accident of which field happened to be a String.

How to correct: The natural order is a product decision, not an implementation convenience. Ask “what does the system’s primary view sort by?” and use that. If the convenient ordering also happens to be correct, use it with confidence. If not, write the slightly harder compareTo or provide only Comparators.

Source: Bloch.EJ3.Item-14 (“consistent with equals” and “principle of least astonishment”); pitfalls.md.


Misconception: implementing Comparable on a mutable field is fine

Wrong mental model: “I will sort Task objects by priority, and I can change priority later; TreeSet will handle it.”

Why it breaks on a small example: Place two Task objects into a TreeSet<Task> where compareTo reads priority. Then change one task’s priority. Call contains() on the set: it may return false even though the object is still there, because the set’s internal tree was indexed on the old priority value. The object is now “lost” inside the set.

How to correct: Either make the field used by compareTo immutable (final, set in the constructor, never mutated), or do not use TreeSet/TreeMap with this type. If priority must be mutable, store the tasks in a plain List and sort with a Comparator each time you need order.

Source: TreeSet javadoc: “the ordering maintained by a set must be consistent with equals”; Bloch.EJ3.Item-17 (minimize mutability).


Formal Definition and Interface Contract

From the Comparable javadoc (Java 25):

“This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class’s natural ordering, and the class’s compareTo method is referred to as its natural comparison method.”

“It is strongly recommended (though not required) that natural orderings be consistent with equals.”

Bloch.EJ3.Item-14 extends this with the key design heuristic: implement Comparable only when the class has an obvious natural ordering that is consistent with equals. When in doubt, do not implement it. Provide Comparators instead.

The Comparator javadoc (Java 25) describes a Comparator as a “comparison function which imposes a total ordering on some collection of objects.” A Comparator is a first-class object: it can be stored in a variable, passed to a method, composed with another Comparator, or constructed inline as a lambda.


Mental Model

Think of the class as a passport: compareTo is the name printed on the cover, the one piece of identifying information the passport office decided on for everyone. A Comparator is a boarding pass: it uses whatever field the airline cares about for this particular flight. You can have one passport and many boarding passes. You cannot have two passports for the same person. If the passport office cannot agree on what to print on the cover because the answer depends on the country you are entering, the passport does not issue: that is the type with no natural order, and it should have no compareTo.


Connections

Within CSCD 211: This lesson sits between the two foundation lessons covering what each method does (Natural Order Is implements Comparable\<T\> and Alternate Orders Are Comparator\<T\>) and the named-comparator-class lesson covering how to write a Comparator. The design choice made here determines which of those topics is relevant for a given type.

Looking back: compare Returns a Sign, Not a Magnitude established what compareTo and compare return and what the symmetry/transitivity requirements are. This lesson builds on that contract and asks the prior question: should the ordering live in the type at all?

Looking ahead: Once you know a type needs one or more Comparators, Implementing Comparator\<T\> as a Named Class covers how to write them as standalone classes; the lambda and method-reference lessons cover the shorthands. One Class, Many Comparators by Design shows the full both/and pattern at the scale of a complete lab assignment (F19.Lab1-Review).


Go Deeper (optional)

Nothing here is needed to write correct, working comparison code. Read it when you are curious, not because you feel you have to.

Where this shows up in professional work. In code reviews at most mid-to-large Java shops, the question “why did you implement Comparable here?” comes up regularly. A developer who can answer “because the daily ledger is the system’s primary view, so callers of Arrays.sort should not need to know the internals” demonstrates production-level design judgment rather than just syntax familiarity. Named Comparator constants placed inside the owning class as public static final fields are the idiom you will see in professional codebases, and recognizing that pattern immediately signals readability awareness.

The Comparator factory methods. The Comparator interface in Java 8 and later ships a family of factory methods (Comparator.comparing, thenComparing, reversed, comparingInt) that allow multi-field orderings to be composed without writing a single explicit comparison branch. The BY_TEAM and BY_GAMES_PLAYED_DESC constants in the Player example above use exactly these methods. The lambda and method-reference lessons in this lane cover those shorthands in detail; this material is the design foundation they build on.

The order-theory and design-pattern connections. A Comparable type asserts a canonical total order on its value set: the axioms that compareTo must satisfy (antisymmetry, transitivity, totality) are precisely the axioms of a strict total order from order theory. A Comparator is any element of the space of all total orders on that domain. When a type implements Comparable, it pins one canonical element of that space. When no canonical element exists, the type correctly makes no assertion and leaves the choice to the caller. The function-object pattern behind Comparator is also the Strategy pattern from the Gang of Four (Gamma et al. 1994): the ordering algorithm is encapsulated in a separate object and swapped in at the call site. This is the simplest concrete example of treating a computation (ordering) as a first-class value, a concept that reappears in lambdas, Runnable, and every functional-interface use in Java. The one-responsibility principle (each ordering belongs to one object) traces directly to Parnas (1972), who argued for separating decisions that change independently. Natural order belongs to the type; alternate orderings belong to the contexts that need them: the two kinds of change are independent, so they belong in different places.


Practice

Level 1

You are designing a Money class with a long centsAmount field. Should Money implement Comparable<Money>? Justify in one sentence.

Thought process

Ask the decision question: if a colleague reads Arrays.sort(prices), can they predict the result without looking at the source? For monetary amounts, ascending numeric order is universally expected. There is no competing interpretation.

Show answer

Yes. Ascending numeric value is the only reasonable default for a monetary amount; any caller reading Arrays.sort(prices) would expect smallest-first. Implement Comparable<Money> with compareTo delegating to Long.compare(this.centsAmount, other.centsAmount).


Level 2

A student writes:

public class Player implements Comparable<Player> {
    public int compareTo(Player other) { return this.name.compareTo(other.name); }
    public int compareToByTeam(Player other) { ... }
    public int compareToByPosition(Player other) { ... }
    public int compareToByGamesPlayed(Player other) { ... }
}

Identify two problems with this design and describe the standard fix for each.

Thought process

Look at what each method can do for a caller:

  • compareTo implements Comparable and is recognized by Arrays.sort, TreeSet, etc.
  • compareToByTeam, compareToByPosition, compareToByGamesPlayed are plain methods with no interface backing. No standard library method knows how to call them.

Now ask: can a caller pass compareToByTeam to Arrays.sort? No, because it is not a Comparator. The methods exist but cannot be used where ordering is needed.

Also count how many times Comparable<T> is implemented: once, by name. The extra methods do not add additional natural orders; they just add dead weight. (Source: F19.Lab1-Review gets this design right; pitfalls.md.)

Show answer

Problem 1: The custom compareToByX methods are not usable by any standard Java sorting or collection API. Arrays.sort(arr, comparator) requires a Comparator<Player>, not an ordinary method reference with this signature. The methods are dead weight as written.

Problem 2: The design implies that sorting by team, position, or games played are all properties of the class itself. They are properties of the context (the caller’s current need). Mixing them into the class makes it harder to add new orderings later without touching the class.

Standard fix: Replace each compareToByX method with a named Comparator<Player>:

public static final Comparator<Player> BY_TEAM =
    Comparator.comparing((Player p) -> p.team);

public static final Comparator<Player> BY_POSITION =
    Comparator.comparing((Player p) -> p.position);

public static final Comparator<Player> BY_GAMES_PLAYED_DESC =
    Comparator.comparingInt((Player p) -> p.gamesPlayed).reversed();

These can then be passed directly to Arrays.sort(roster, Player.BY_TEAM). (Source: F19.Lab1-Review, pitfalls.md.)


Level 3

You are designing a Color class with int red, int green, int blue fields (each 0 to 255). A colleague suggests: “Just implement Comparable<Color> by lexicographic RGB order (compare red first, then green, then blue as a tiebreaker). It is unambiguous and easy to remember.”

Evaluate this suggestion. Is lexicographic RGB a natural order for Color? What should the design be instead, and why?

Thought process

Consider whether a caller reading Arrays.sort(palette) would predict lexicographic RGB order. Think about what color orderings are actually used in practice: perceptual brightness, HSL hue, sRGB luminance, alpha compositing order. None of these is lexicographic RGB. The suggestion is unambiguous (internally consistent) but not expected by callers. “Unambiguous” and “natural” are different properties. (Source: objective.md stem 2; pitfalls.md first pitfall.)

Now think about what information is lost or gained. If you implement Comparable<Color> by RGB lex, any caller who writes Arrays.sort(colors) without a Comparator silently gets RGB lex order. They cannot tell from the call site what ordering they are getting. If instead there is no Comparable and only named Comparators (Color.BY_BRIGHTNESS, Color.BY_HUE, Color.BY_RED), the call site communicates intent explicitly.

Show answer

Lexicographic RGB is unambiguous but not natural. There is no established convention that “the default order for colors is by red channel, then green, then blue.” A caller reading Arrays.sort(palette) would not predict this result without looking at the source. That fails the decision-question test.

The suggestion also encodes the implementer’s convenience (RGB lex is easy to write) as the type’s identity, which is the second pitfall from Bloch.EJ3.Item-14: natural order should reflect caller expectation, not implementation convenience.

Correct design: do not implement Comparable<Color>. Provide named Comparators:

public static final Comparator<Color> BY_RED =
    Comparator.comparingInt((Color c) -> c.red);

public static final Comparator<Color> BY_BRIGHTNESS =
    Comparator.comparingInt((Color c) -> c.red + c.green + c.blue);

public static final Comparator<Color> BY_HUE = ...; // HSL conversion

Every call site must now name what it is sorting by, which makes the code self-documenting and prevents silent surprises. (Source: objective.md stem 2; Bloch.EJ3.Item-14.)


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 test does this lesson recommend to decide whether a type should implement Comparable?

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

A Color class has red, green, and blue int fields. According to the lesson, what is the correct design for ordering Color objects?

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

A student writes compareToByTeam, compareToByPosition, and compareToByGamesPlayed as ordinary public methods inside a Player class. What is wrong with this design?

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

Two Task objects are added to a TreeSet<Task> where compareTo reads the mutable priority field. After insertion, task1.priority is changed from 1 to 99. What does treeSet.contains(task1) return, and why?

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

The Receipt example in the lesson chooses timestamp as the natural order. Which justification matches the lesson’s reasoning?

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