← Skill tree CS Skill Tree 0 CSCD211

One Class, Many Comparators by Design

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. Natural order limit. How many natural orderings can a class declare?

Check

At most one. A class can implement Comparable<T> for only one T, so it can have at most one compareTo method, which means at most one natural order.

2. Comparator limit. How many Comparator<T> classes can exist for the same type T?

Check

Unlimited. Each Comparator<T> is a separate class or expression, independent of the type T. There is no upper bound.

3. Modification cost. If three Comparator<Player> classes exist and a fourth ordering is needed, what files must change?

Check

Only one new file is added: the fourth comparator class. Player.java and the three existing comparators are untouched.


Try This First

F19 Lab 1 contains this directory listing:

cscd211Classes/
    Player.java
cscd211Comparators/
    TeamComparator.java
    PositionComparator.java
    GamesPlayedComparator.java

A new requirement asks for sorting by jersey number. Before reading further: list every file that needs to change.

Check

Only one new file: cscd211Comparators/JerseyNumberComparator.java. Player.java and the three existing comparators are untouched. This is the open-closed property in action.


What You Need To Walk In With

The Insight: One natural order is a property of the type (one fact about the type itself). Multiple comparators are properties of use cases (different contexts that need different orderings). Use cases multiply; the type does not. The design pattern of many comparators per class is not redundancy; it is the correct architecture for a type with multiple legitimate orderings.

Each comparator file is typically 10 to 15 lines. The cost is low; the benefits are concrete: each ordering is independently testable, carries a named class that documents its purpose, and requires no change to the type being ordered. You can: explain the one-vs-many asymmetry between Comparable and Comparator, identify the design smell of placing custom comparison methods directly on a type, articulate why multiple comparators are correct architecture rather than redundancy, and design a package of comparators for any type given a set of ordering requirements.


How It Works

The asymmetry: why one vs. many

Comparable<T> is implemented by the type itself. It says: “I have one default ordering.” That ordering lives inside the class file. A class can make that promise exactly once.

Comparator<T> is a separate object. It says: “Here is a strategy for ordering objects of type T.” The strategy lives outside the class. Any number of strategies can exist for the same type, each created independently, each packaged in its own class, and each unknown to the type being ordered.

This asymmetry is not an accident. It reflects the difference between “who I am” (the type’s natural identity) and “how you want to use me” (context-specific ordering).

F19 Lab 1: the canonical example

Player in F19 Lab 1 has four orderings defined:

Ordering Lives in Mechanism
By last name Player.compareTo inside Player.java Comparable<Player>
By team TeamComparator.java Comparator<Player>
By position PositionComparator.java Comparator<Player>
By games played GamesPlayedComparator.java Comparator<Player>

Player.java has no knowledge of the three comparator classes. If all three comparator files are deleted, Player.java still compiles and runs.

Open-closed at the ordering level

Each new ordering requirement produces one new file. No existing file changes. The design is:

This makes adding a fifth or sixth ordering free of risk: the existing sort calls remain correct.

The design smell: custom compareToByX methods

The opposite of this pattern is placing multiple comparison methods on the type itself:

// SMELL: compareToByTeam, compareToByPosition, etc. on Player
public int compareToByTeam(Player other) { ... }
public int compareToByPosition(Player other) { ... }

These methods are invisible to Arrays.sort (which recognizes only compareTo from Comparable<T>) and invisible to sorted collections. They accumulate in the class with no standard caller. The fix is always: move each one into its own Comparator<Player> class.

Source: Bloch, Effective Java, Item 14.


Quick check

Check your understanding

Player.java implements Comparable<Player>. Three separate Comparator<Player> classes also exist. A new ordering by jersey number is needed. Which statement correctly describes how many Comparator<Player> objects can exist for Player?

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


Worked Example: Predict Then Check

Given this setup (from SP19.Lab1-warm-up):

cscd211Classes/Player.java             -- Player implements Comparable<Player>
cscd211Comparators/TeamComparator.java
cscd211Comparators/PositionComparator.java
cscd211Comparators/GamesPlayedComparator.java

The client code runs:

Player[] nflPlayers = loadRoster();

// Sort 1
Arrays.sort(nflPlayers);

// Sort 2
Arrays.sort(nflPlayers, new TeamComparator());

// Sort 3
Arrays.sort(nflPlayers, new PositionComparator());

For each sort: what ordering is used, and what mechanism provides it?

Reasoning

Sort 1: no comparator; uses Player.compareTo, which is the natural order (by last name). Sort 2: TeamComparator.compare; comparator is passed as the second argument. Sort 3: PositionComparator.compare; another comparator.

All three sorts on the same array, with no change to Player.java. The same object participates in three orderings defined independently.

Show answer
Sort Ordering Mechanism
1 Last name (natural order) Player.compareTo
2 Team name TeamComparator.compare
3 Position PositionComparator.compare

Source: SP19.Lab1-warm-up.


Common Misconceptions

Misconception: multiple Comparators for one class is a code smell

Wrong mental model: “Three Comparators for Player? That is redundant. One of them should be the natural order.”

Why it is wrong: Multiple orderings are not redundant when they represent distinct, legitimate use cases. Football data has team-based views, position-based views, and games-played views. None is obviously more natural than the others. Forcing one into compareTo would make the natural order an arbitrary choice, not a meaningful contract.

How to correct: Accept that the multiplicity is the design. Each comparator is small, named, and self-documenting. The cost is one file per ordering. The benefit is that each ordering can be used, tested, and changed independently.

Source: Bloch, Effective Java, Item 14; F19.Lab1-Review.

Quick check

Check your understanding

A student says: ‘Player has TeamComparator, PositionComparator, and GamesPlayedComparator. That is three files just for sorting -- one of them should be folded into Player.compareTo to reduce clutter.’ What is the flaw in this reasoning?

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


Formal Definition and Interface Contract

From the Java 25 Comparator API:

Comparators can be passed to a sort method (such as Collections.sort or Arrays.sort) to allow precise control over the sort order. Comparators can also be used to control the order of certain data structures (such as sorted sets or sorted maps), or to provide an ordering for collections of objects that don’t have a natural ordering.

The API explicitly acknowledges that a type can have multiple comparators and that comparators serve use-case-specific control.


Mental Model

Think of a type as a filing system. The natural order is the default filing rule: alphabetical by subject. A comparator is an ad-hoc sort request from a specific user: “for this report, sort by date.” Both can coexist. The filing system (the type) does not change when a new sort request (a new comparator) arrives. The request is self-contained and disposable.


Connections

Within CSCD 210/211: This lesson revisits the natural order vs. comparator dual from the perspective of the full F19 Lab 1 design. The labs from F17, SP18, SP19, W17, and W18 all use the same many-comparators pattern.

Looking back: Natural Order Is implements Comparable<T> introduced the dual. Implementing Comparator<T> as a Named Class showed how to write a named comparator. This lesson synthesizes both into a design perspective.

Looking ahead: Static Comparator Fields as Named Orders covers the modern idiom of declaring comparators as static final fields on the type being compared, giving them stable, reachable names without requiring new at every call site.


Go Deeper (optional)

None of this depth is needed to write correct, working comparator code. It is here for those who enjoy knowing where ideas come from.

The open-closed principle (open for extension, closed for modification) that this design illustrates is one of the five SOLID design principles, originally articulated by Bertrand Meyer in his 1988 book Object-Oriented Software Construction and later popularized by Robert Martin. The comparator layout is a textbook illustration: the extension point is “add a comparator file,” and the closed component is the type being compared. Recognizing this connection makes the abstract principle concrete and gives you a real example to cite in any discussion of design principles.

At a deeper level, a Comparator<T> is a binary relation on T that imposes a total order. The contract rules (antisymmetry, transitivity, totality) are the axioms of a strict total order in mathematics. The compare method is a witness that the relation exists. When a comparator violates one of those axioms (for example, by returning inconsistent results for the same pair on different calls), sorted data structures such as TreeSet and Arrays.sort produce undefined behavior, not a friendly error.

The Comparator interface is also the simplest first-class function in Java’s pre-lambda history. Passing a new TeamComparator() to Arrays.sort is passing a function as data: the Strategy design pattern at its most minimal. When Java 8 introduced lambdas, the many-comparators design remained valid, but the implementation became one line: Comparator.comparing(Player::getTeam) instead of a full named class. Both forms express the same structural idea.

In professional Java codebases, large data-layer classes commonly support dozens of sort orders for different reports, API endpoints, and UI views, all without touching the data class itself. The many-comparators pattern scales to that setting directly.


Practice

Level 1

F19 Lab 1 has three Comparators for Player. A new requirement arrives: sort players by jersey number ascending. List every file that must be created or changed.

Show answer

One new file: cscd211Comparators/JerseyNumberComparator.java.

Zero existing files change. Player.java and the three existing comparators are untouched.


Level 2

A student has a Receipt class with compareTo (natural order: by timestamp). The student adds methods compareToByAmount(Receipt other), compareToByCustomer(Receipt other), and compareToByTax(Receipt other) directly to Receipt. Name the design smell, state what is wrong with each method, and provide the correct structure.

Show answer

Smell: comparison logic on the type. Each method is invisible to Arrays.sort and sorted collections.

What is wrong: Arrays.sort(receipts) uses compareTo. Arrays.sort(receipts, cmp) uses a Comparator. There is no standard mechanism that calls compareToByAmount. The methods accumulate in the class with no standard callers.

Correct structure:

cscd211Classes/Receipt.java               -- compareTo by timestamp (natural order)
cscd211Comparators/ReceiptByAmountComparator.java
cscd211Comparators/ReceiptByCustomerComparator.java
cscd211Comparators/ReceiptByTaxComparator.java

Each comparator is a separate file implementing Comparator<Receipt>.


Level 3

A colleague claims: “The more comparators a class has, the worse the design. A well-designed class should have at most two orderings: the natural order and one alternate.” Refute this claim with a concrete example from the lab history.

Show answer

The claim is incorrect. F19 Lab 1 (F19.Lab1-Review) defines three comparators for Player, none of which are redundant. Football statistics are viewed by team in standings reports, by position in roster analysis, and by games played in injury-risk assessments. All three are distinct, legitimate orderings used in different contexts. No one of them is obviously more natural than the others, so none belongs in compareTo. The design is correct because each ordering is independent, testable, and adds no coupling to Player.java.

The claim confuses “few classes” with “good design.” The correct criterion is: each ordering should be motivated by a real use case and should live outside the type. The number of comparators follows from the number of use cases, which the domain determines.


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

F19 Lab 1 defines three Comparator classes for Player (TeamComparator, PositionComparator, GamesPlayedComparator) plus Player.compareTo. A new requirement asks for sorting by jersey number. How many existing files must change?

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

Which statement correctly describes the asymmetry between Comparable and Comparator?

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

Given this client code from the lesson example: Player[] nflPlayers = loadRoster(); Arrays.sort(nflPlayers); Arrays.sort(nflPlayers, new TeamComparator()); Arrays.sort(nflPlayers, new PositionComparator()); After all three sorts run, what ordering does nflPlayers have, and what mechanism produced it?

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

A student writes compareToByAmount, compareToByCustomer, and compareToByTax directly on a Receipt class. What design smell does this illustrate, and what is the core problem?

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

A colleague claims: ‘Three Comparators for one class is redundant. A well-designed class should have at most one alternate ordering.’ Which response best refutes this using the F19 Lab 1 example?

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