One Class, Many Comparators by Design
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:
- Open for extension: new orderings can be added by adding a new file.
- Closed for modification: existing types and comparators are not touched.
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?
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?
Formal Definition and Interface Contract
From the Java 25 Comparator API:
Comparators can be passed to a sort method (such as
Collections.sortorArrays.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?
Which statement correctly describes the asymmetry between Comparable and Comparator?
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?
A student writes compareToByAmount, compareToByCustomer, and compareToByTax directly on a Receipt class. What design smell does this illustrate, and what is the core problem?
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?