Implementing Comparator<T> as a Named Class
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. Interfaces and implements. What does the compiler require of a class that says implements Comparator<Player>?
Check
The class must provide a concrete method for every abstract method declared by Comparator<Player>. The primary abstract method is int compare(Player a, Player b). If the class does not provide that method, the code will not compile.
2. The @Override annotation. A student writes:
public int compareto(Player a, Player b) { ... }
They claim @Override is optional. What happens if they add @Override to this line?
Check
The compiler rejects the code immediately, because no method named compareto exists in Comparator<Player> to override. The annotation turns a silent mistake into a compile error. That is exactly why it is required. (See Bloch, Effective Java, 3rd ed., Item 40, and the Java 25 @Override docs.)
3. Precondition checks. Why would a method throw IllegalArgumentException instead of letting a NullPointerException happen on its own?
Check
A self-thrown IllegalArgumentException with a descriptive message (“Bad Player in TeamComparator.compare”) tells the caller exactly which class and method received a bad argument. A spontaneous NullPointerException from inside a field access gives a stack trace that does not name the comparator at all, making debugging slower.
Try This First
You have a list of Player objects. Each player has a getTeam() method that returns a String. You want to sort the list by team name, alphabetically.
Before reading further: sketch the four things a class needs to do this job. Do not think about lambdas or method references yet. Think about a plain class with one method.
Write them down, then expand the next section to compare.
What four things did you list?
- Declare that it implements
Comparator<Player>. - Provide a method named
comparethat takes twoPlayerparameters and returns anint. - Return a negative number when the first player’s team comes before the second, zero when they are equal, and a positive number otherwise.
- Guard against null arguments before touching any field.
Everything else (the class name, the package, the import) is scaffolding for those four things.
What You Need To Walk In With
A Comparator class is a function object: an instance whose entire job is to compare two values and return the sign. The class exists not to hold data about players or books or sandwiches, but to carry a single comparison strategy that can be passed around and swapped out at any time.
Understanding this pattern means more than copying the skeleton. It means knowing what implements demands from the compiler, what sign each return value must carry, and why the precondition check produces a more useful error message than letting the JVM crash on its own. With this foundation in place, you can: write any named comparator class that satisfies the full coding standard, trace what happens when compare receives a null argument, identify every violation in a faulty comparator and state the exact fix, and predict what Arrays.sort will produce given a comparator instance.
How It Works
The Comparator interface at a glance
Comparator<T> is a one-method interface (ignoring default methods for now). Its contract is:
int compare(T a, T b)
Returns a negative integer, zero, or a positive integer according to whether a is less than, equal to, or greater than b in the ordering this comparator defines. (Java 25 Comparator docs)
The canonical skeleton
Here is the standard form for a comparator that orders Player objects by team name. This skeleton comes from F19.Lab1-Review (cscd211Comparators/TeamComparator.java).
First time seeing implements, @Override, or the <Player> in angle brackets? Open for a 20-second refresher.
- An interface is a list of methods a class promises to provide.
implements Comparator<Player>is that promise: the class is now required to definecompare. The full treatment is in the implements keyword and Comparable. @Overridetells the compiler this method is meant to replace one the interface declares, so a misspelled name becomes a compile error instead of a silent bug.- The
<Player>is a type parameter: it pins this comparator toPlayerobjects, so the compiler checks the types for you instead of you casting by hand.
package cscd211Comparators;
import java.util.Comparator;
import cscd211Classes.Player;
public class TeamComparator implements Comparator<Player>
{
@Override
public int compare(final Player a, final Player b)
{
if (a == null || b == null)
throw new IllegalArgumentException("Bad Player in TeamComparator.compare");
return a.getTeam().compareTo(b.getTeam());
}
}
Every line carries weight. The table below traces each one:
| Line | What it does | Why it is required | ||
|---|---|---|---|---|
package cscd211Comparators; |
Places the class in the right package | Convention for all comparator classes in this course; see the package-convention lesson for the full rule | ||
import java.util.Comparator; |
Brings the interface into scope | Without it, Comparator<Player> does not resolve |
||
implements Comparator<Player> |
Declares the contract | The compiler now requires a compare(Player, Player) method; <Player> provides type safety |
||
@Override |
Verifies the name against the interface | Catches compareTo vs compare confusion at compile time |
||
final Player a, final Player b |
Marks parameters immutable | Coding standard; prevents accidental mutation of the comparands inside the method | ||
| `if (a == null | b == null)` | Null precondition check | Arrays.sort can pass null elements; the IAE gives a named source in its message |
|
return a.getTeam().compareTo(b.getTeam()) |
Delegates to String’s natural order | String.compareTo already satisfies the sign contract; no arithmetic needed |
An analogy: the sorting hat is a function object
Imagine a factory floor where items arrive on two conveyor belts. A worker standing between them is handed two items at a time, compares them, and holds up a card: “left first,” “right first,” or “tie.” That worker is the comparator. The worker has no data about the items before they arrive; the worker only knows one thing: how to compare.
A Comparator class is that worker made into a Java object. It holds no mutable state. It receives two values, reads the relevant field, and returns the sign.
Quick check
Check your understanding
Which of the following is the complete, correct class declaration for a comparator that orders Player objects?
Worked Example: Predict Then Check
The setup
Here is a second comparator from the same lab, ordering Player objects by games played (ascending). Read the code, predict the output of the three calls below it, then check your predictions.
package cscd211Comparators;
import java.util.Comparator;
import cscd211Classes.Player;
public class GamesPlayedComparator implements Comparator<Player>
{
@Override
public int compare(final Player a, final Player b)
{
if (a == null || b == null)
throw new IllegalArgumentException("Bad Player in GamesPlayedComparator.compare");
return Integer.compare(a.getGamesPlayed(), b.getGamesPlayed());
}
}
Source: F19.Lab1-Review, cscd211Comparators/GamesPlayedComparator.java.
Assume playerA.getGamesPlayed() returns 10 and playerB.getGamesPlayed() returns 25.
GamesPlayedComparator c = new GamesPlayedComparator();
int r1 = c.compare(playerA, playerB); // predict: negative, zero, or positive?
int r2 = c.compare(playerB, playerA); // predict?
int r3 = c.compare(playerA, playerA); // predict?
Step-by-step reasoning
Integer.compare(x, y) returns a negative int when x < y, zero when x == y, and a positive int when x > y.
r1:Integer.compare(10, 25)returns a negative value (10 is less than 25).r2:Integer.compare(25, 10)returns a positive value (25 is greater than 10).r3:Integer.compare(10, 10)returns zero (same object, same value).
Show answer
r1 < 0, r2 > 0, r3 == 0.
When Arrays.sort receives this comparator, it treats a negative result as “a should come before b.” So playerA (10 games) will be sorted before playerB (25 games): ascending order by games played.
Step-by-step algorithm for writing any named comparator
- Choose a name:
<Domain>By<Field>Comparatoris conventional (for example,PlayerByTeamComparator). Labs often use the shorter<Field>Comparatorwhen the domain is clear. - Write the package declaration for
cscd211Comparators. - Add
import java.util.Comparator;and the import for your domain class. - Declare
public class ... implements Comparator<YourClass>. - Write the method signature with
@Override,finalon both parameters, andintreturn type. - Add the null precondition check, throwing
IllegalArgumentExceptionwith class and method in the message string. - Write one return statement that delegates to an existing comparison method (
compareToon aString,Integer.compareforint,Double.comparefordouble).
Common Misconceptions
Misconception 1: naming the method compareTo instead of compare
Wrong mental model: “I am comparing things, so the method is
compareTo, same asComparable.”
Why it breaks on a small example: Compile the class below:
public class PositionComparator implements Comparator<Player>
{
public int compareTo(Player a, Player b) // BUG
{
return a.getPosition().compareTo(b.getPosition());
}
}
Without @Override, the compiler accepts this class, but Comparator<Player> is not actually implemented. The method named compareTo(Player, Player) is a new method unrelated to the interface. Passing a PositionComparator to Arrays.sort produces a compile error there (“cannot infer type arguments”).
With @Override, the compiler rejects the class at the declaration line, immediately pointing to the mistake.
How to correct: Always write compare, never compareTo, in a Comparator class. Always include @Override.
Source: direct observation from F19.Lab1 grading; Bloch, Effective Java, 3rd ed., Item 40.
Misconception 2: using raw Comparator without a type parameter
Wrong mental model: “
implements Comparatoris shorter and it works.”
Why it breaks on a small example: With implements Comparator (raw), the required method signature becomes compare(Object, Object). Every access to a field requires a cast:
Player p1 = (Player) a;
Player p2 = (Player) b;
The compiler accepts those casts. But if the array passed to Arrays.sort accidentally contains a Book object, the cast fails at runtime with ClassCastException. The type parameter <Player> in implements Comparator<Player> moves that error to compile time, where it belongs.
How to correct: Always write implements Comparator<YourClass>.
Source: Bloch, Effective Java, 3rd ed., Item 26.
Misconception 3: mutating a comparand inside compare
Wrong mental model: “I can normalize the data before comparing, such as uppercasing the team name or rounding the number.”
Why it breaks on a small example: Arrays.sort calls compare many times during sorting. If the method writes back to a or b between calls, the data changes underneath the sort algorithm. The resulting order is unspecified, and the original objects are silently corrupted.
How to correct: compare is a pure function. If a normalized form is needed (case-insensitive comparison, for instance), compute it into a local variable:
String teamA = a.getTeam().toLowerCase();
String teamB = b.getTeam().toLowerCase();
return teamA.compareTo(teamB);
The local variables are not part of the objects; writing to them is safe. Never write to a or b.
Source: Bloch, Effective Java, 3rd ed., Item 14; Java 25 Comparator javadoc.
Misconception 4: skipping the null precondition check
Wrong mental model: “
Arrays.sortwill not pass null elements, so the check is unnecessary.”
Why it breaks on a small example: If the array contains a null element, the call compare(null, somePlayer) reaches the method body. The first field access, a.getTeam(), throws a NullPointerException. The stack trace points inside TeamComparator, but the message says nothing about which class sent the bad argument.
The IllegalArgumentException with a named message (“Bad Player in TeamComparator.compare”) is more diagnostic. It is also required by the course coding standard.
How to correct: Match the skeleton exactly:
if (a == null || b == null)
throw new IllegalArgumentException("Bad Player in TeamComparator.compare");
Substitute the actual class and method names in the message string.
Source: S20.CourseDoc.Comparable_Comparator-StudentVersion; F19.Lab1.CSCD211Lab1Methods.java precondition pattern.
Quick check
Check your understanding
A student writes ‘public int compareTo(Player a, Player b)’ inside a class that says ‘implements Comparator<Player>’, without @Override. What happens when this class is compiled?
Formal Definition and Interface Contract
From the Java 25 Comparator API:
A comparison function, which imposes a total ordering on some collection of objects. Comparators can be passed to a sort method (such as
Collections.sortorArrays.sort) to allow precise control over the sort order.
int compare(T o1, T o2)-- Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
Additional contract requirements (from the same source):
- Antisymmetry and transitivity:
sgn(compare(x, y)) == -sgn(compare(y, x)); and ifcompare(x, y) > 0andcompare(y, z) > 0, thencompare(x, z) > 0. - Consistency with equals: if
compare(x, y) == 0, thensgn(compare(x, z)) == sgn(compare(y, z))for allz.
These rules are what the compare-method contract covers in depth. Delegating to an existing compareTo or to Integer.compare / Double.compare inherits all three properties for free.
On method naming: JLS §9.4.1 governs how interface methods are inherited and overridden. The @Override annotation (Java 25, java.lang.Override, docs) enforces at compile time that the method actually overrides something declared in a supertype.
Mental Model
Picture a Comparator class as a sealed envelope that contains exactly one question: “given two things of type T, which one comes first?” The envelope has no fields of its own. You hand it to Arrays.sort, and sort opens the envelope every time it needs to decide between two elements. Because the envelope carries no state of its own, you can create one instance and reuse it for every sort call, or create a fresh one each time. The only thing that matters is the answer it returns.
Connections
Within CSCD 210/211: The @Override annotation discipline introduced in CSCD 210 applies here directly. The precondition-check pattern (throw IllegalArgumentException with a class-and-method message) is the same standard used in every method you have written since week one of 210.
Looking back: This builds on the compare-method contract. You need to know what sign the return value must carry before you can write a correct body.
Looking ahead: Once you can write a named comparator, the package-convention lesson establishes where this class lives in the project directory. After that, lambda expressions as comparators are one-liners that replace this entire class when the comparison logic is simple enough.
Go Deeper (optional)
Everything above is what you need to write correct, professional comparator code. The ideas here go further; none of them affect whether your comparator compiles or sorts correctly.
Every professional Java codebase that does custom sorting relies on this pattern. Named comparator classes appear wherever sorting logic is complex enough to deserve its own name and its own test: sorting search results by relevance score, ordering database rows by a compound key, or driving a priority queue in a scheduling system. When you see implements Comparator<Foo> in a production code review, you know immediately that this class has exactly one job, holds no mutable state, and its correctness is defined by three contract axioms.
The deeper structure is worth noticing. A Comparator<T> is precisely a binary relation that imposes a strict total order on its type. The three contract requirements (antisymmetry, transitivity, consistency with equals) are not arbitrary rules; they are the axioms that mathematicians use to define a total order in order theory. When you delegate to String.compareTo or Integer.compare, you are borrowing a proof that those axioms already hold for the delegated type, which is why a one-line body is enough.
The design here is also a well-known pattern from software engineering. A class whose sole purpose is to carry one replaceable algorithm, with no data of its own, is the Strategy pattern (Gamma et al., Design Patterns, 1994). Passing a TeamComparator to Arrays.sort is exactly passing a strategy to an algorithm that does not care which strategy it receives. When Java 8 introduced lambda expressions, it did not replace this idea; it gave a shorter syntax for simple cases. The named-class form remains valuable when you need to give the comparator a meaningful name, test it in isolation, or share it across many call sites without repeating the logic. Lambda expressions and named classes are two spellings of the same underlying concept.
The one-responsibility design visible here traces to David Parnas’s 1972 paper on information hiding: each module should do one thing and hide the decisions that might change. A comparator class hides exactly one decision, the ordering criterion, and nothing else. That is why swapping TeamComparator for GamesPlayedComparator requires no changes to the sorting code at all.
If you want to go further in the Java API, Comparator.comparing and thenComparing (see the Java 25 Comparator docs) build comparators from method references and chain them together without writing a named class; building a comparator by key is the next step here. They are built on exactly the same compare contract you have just learned.
Practice
Level 1
Given:
package cscd211Classes;
public class Book
{
public String getISBN() { ... }
}
Write a complete BookByISBNComparator in package cscd211Comparators that orders Book objects in ascending ISBN order. Include all required elements of the coding standard.
Thought process
Follow the seven-step algorithm from the Worked Example section:
- Name:
BookByISBNComparator(orBookISBNComparator). - Package:
cscd211Comparators. - Imports:
java.util.Comparatorandcscd211Classes.Book. - Declaration:
implements Comparator<Book>. - Method:
@Override public int compare(final Book a, final Book b). - Precondition: throw
IllegalArgumentExceptionif either argument is null. - Return: delegate to
String.compareTo, which gives ascending lexicographic order.
Show answer
package cscd211Comparators;
import java.util.Comparator;
import cscd211Classes.Book;
public class BookByISBNComparator implements Comparator<Book>
{
@Override
public int compare(final Book a, final Book b)
{
if (a == null || b == null)
throw new IllegalArgumentException("Bad Book in BookByISBNComparator.compare");
return a.getISBN().compareTo(b.getISBN());
}
}
This pattern matches the shape of S20.Lab1-Comparator (cscd210comparators/BookISBNComparator.java).
Level 2
A student submits this comparator:
package cscd211Comparators;
import java.util.Comparator;
import cscd211Classes.Player;
public class TeamComparator implements Comparator
{
public int compare(Player a, Player b)
{
return a.getTeam().compareTo(b.getTeam());
}
}
Identify every violation of the coding standard and the interface contract. For each one, state the exact fix.
Thought process
Go through the canonical skeleton line by line and look for differences:
- Is the type parameter present?
- Is
@Overridepresent? - Are parameters marked
final? - Is there a precondition check?
Each missing item is a separate violation.
Show answer
There are four violations:
- Raw type.
implements Comparatorshould beimplements Comparator<Player>. Without<Player>, the compiler inferscompare(Object, Object)and the existingcompare(Player, Player)method does not override the interface method. Fix: add<Player>.
- Missing
@Override. The method does not override the parameterized interface method (because of the raw-type bug above). Even after fixing the raw type,@Overridemust be present to guarantee the name and signature are correct. Fix: add@Overrideabove the method.
- Parameters not
final. Bothaandbshould be declaredfinal. Fix: writefinal Player a, final Player b.
- No precondition check. If either argument is null,
a.getTeam()will throw a namelessNullPointerException. Fix: add the standard null check and throwIllegalArgumentException("Bad Player in TeamComparator.compare").
Source: pattern from F19.Lab1 grading observation (see objective.md Stem 2).
Level 3
You are asked to write a PlayerByWinsDescendingComparator that orders Player objects by getWins() (an int accessor), with the player who has the most wins appearing first in a sorted array.
Descending order means: when a has more wins than b, the comparator must return a negative number so that a sorts before b.
Write the complete class. Then explain in one sentence why you cannot use subtraction (a.getWins() - b.getWins()) to get descending order, and what you use instead.
Thought process
For ascending order you would write Integer.compare(a.getWins(), b.getWins()). To reverse, you swap the arguments: Integer.compare(b.getWins(), a.getWins()). When b has more wins, Integer.compare(b, a) returns positive, but that means a sorts after b, which is descending.
The subtraction trick (b.getWins() - a.getWins()) gives the right sign most of the time, but it overflows when the two values are far apart (for example, Integer.MIN_VALUE - 1). Integer.compare never overflows. (The subtraction trap is covered more fully in the overflow lesson.)
Show answer
package cscd211Comparators;
import java.util.Comparator;
import cscd211Classes.Player;
public class PlayerByWinsDescendingComparator implements Comparator<Player>
{
@Override
public int compare(final Player a, final Player b)
{
if (a == null || b == null)
throw new IllegalArgumentException(
"Bad Player in PlayerByWinsDescendingComparator.compare");
return Integer.compare(b.getWins(), a.getWins());
}
}
Subtraction (b.getWins() - a.getWins()) produces the wrong sign when integer overflow occurs; Integer.compare(b.getWins(), a.getWins()) is overflow-safe and returns the correct sign in all cases.
Overflow risk: Bloch, Effective Java, 3rd ed., Item 14; W18.Lab1.WattsSort is a positive example of using Double.compareTo correctly instead of subtraction.
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 method name does Comparator<T> require you to implement?
A student writes ‘implements Comparator’ without a type parameter. What is the primary risk?
In a correctly written named comparator, why are the two parameters declared final?
Given GamesPlayedComparator from the lesson, where playerA.getGamesPlayed() returns 10 and playerB.getGamesPlayed() returns 25, what does ‘c.compare(playerB, playerA)’ return?
What is the correct null precondition check for a TeamComparator, matching the canonical skeleton shown in this lesson?