Writing the compareTo Body: One Key, Then Several
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? Read Natural Order Is implements Comparable<T> first.
That lesson covers the class header, the method signature, and the sign contract. This one picks up right after: you have the header, and now you have to fill in the body.
Try This First
You added implements Comparable<Player> to a class, and the compiler underlines it in red:
Class 'Player' must either be declared abstract or implement abstract method 'compareTo(T)' in 'Comparable'
The declaration is a promise to write one method. Before reading on, answer two questions on paper: which field should decide the order, and what single line would compare that field?
What did you write?
Those two questions are the entire method. Pick the field (or fields), then write one repeating line per field. A roster sorts by last name, so the field is lastName and the line delegates to String.compareTo. Everything below builds from that.
What You Need To Walk In With
Two things stop students cold when they open an empty compareTo: knowing which field to compare, and knowing what to physically type in the body. Both have a plain answer. The sort key is decided by the problem, not invented; the body is one repeating move, one line per field, guarded so ties fall through to the next field.
Coming in, you should be able to write a one-field compareTo that delegates to String.compareTo, and to state the sign rule (negative means before, zero means tie, positive means after). By the end you can choose the fields for any class, order them, and write the full cascade for one, two, or three keys.
How It Works
Choosing what to compare
The sort key is not something you invent on the spot. It is decided by a plain question: when these objects are lined up, what makes one come before another? Two sources answer it.
The instructions usually say it outright. Most assignments hand you the keys and their order in one sentence. A real CSCD 211 practice task states: “Students should first be compared by last name for order. If the last names are the same, compare by first name. If first names are the same, compare by studentId.” Read that sentence and turn each named field into one step, in the order given: last name, then first name, then studentId. The English becomes the body almost word for word.
When the instructions are silent, ask what a person would do. Picture two of these objects sitting next to each other and ask which one a person would put first, and why. That field is the primary key. Two books on a shelf go by author, then title. Two names on a roster go by last name, then first name. The next field is the tie-breaker they reach for when the first one matches.
A starting table you can borrow from. These are suggestions; the instructions win when they say something different.
| The object | A sensible order (primary first) |
|---|---|
| A word or label (a Widget, a BoxCar) | the text itself |
| A person (an Author, a Player) | last name, then first name |
| A publisher | name |
| A book | author or publisher, then title, then isbn |
| An engine | horsepower, then manufacturer |
| An employee | type, then salary |
Order the chosen fields from most important to least. The first key decides most pairs. Each later key matters only when the earlier ones tie. Make the last key something unique when you can (an id, an isbn), so no two different objects ever tie all the way down.
Comparing one field
For every key, you make the same move: ask the field to compare itself, then act on the sign.
- A
String, anenum, or another object that already implementsComparable: call itscompareTo, as inthis.last.compareTo(other.last).Stringalready sorts alphabetically, so you delegate to it. Everyenumhas a built-incompareTothat orders by declaration position. - A number (
int,double,long): callInteger.compare(a, b)orDouble.compare(a, b). Never subtract (subtraction can overflow and flip the sign).
Each of these returns the same kind of answer: a negative int, zero, or a positive int.
Gluing several fields together: the cascade
Compare the first key. If the result is not zero, you have your answer, so return it. If it is zero (a tie), move to the next key. Return the last comparison directly, with no guard.
int byFirstKey = /* compare the first field */;
if (byFirstKey != 0)
{
return byFirstKey; // not a tie, so this settles it
}
// still here means the first key tied; try the next
return /* compare the last field */;
The one line students drop is if (result != 0) return result;. Without it, control falls past the first key and the object is ordered by the last field alone.
Worked Example: The Ladder
Every rung is the same shape. The only thing that grows is the number of keys. Read them top to bottom and watch the pattern repeat.
One key: a single String
A Widget holds one String and sorts by it. The body is a single delegated line.
public class Widget implements Comparable<Widget>
{
private String contents;
@Override
public int compareTo(final Widget other)
{
return this.contents.compareTo(other.contents);
}
}
That is the entire method. String does the work; you hand it the other object’s field.
Two keys: last name, then first name
An Author sorts by last name, and breaks ties by first name. One guard appears.
public class Author implements Comparable<Author>
{
private String first, last;
@Override
public int compareTo(final Author other)
{
int byLast = this.last.compareTo(other.last);
if (byLast != 0)
{
return byLast;
}
return this.first.compareTo(other.first);
}
}
Compare last names. If they differ, that decides it. If they match, the first name breaks the tie.
Two keys, one of them a number
An Engine sorts by horsepower (an int), then by manufacturer. The number field uses Integer.compare, not subtraction.
public class Engine implements Comparable<Engine>
{
private int horsePower;
private String manufacturer;
@Override
public int compareTo(final Engine other)
{
int byPower = Integer.compare(this.horsePower, other.horsePower);
if (byPower != 0)
{
return byPower;
}
return this.manufacturer.compareTo(other.manufacturer);
}
}
Subtracting this.horsePower - other.horsePower looks shorter and works on small numbers, so it is tempting. It breaks when the two values are far apart. An int holds 32 bits; if the true difference is larger than Integer.MAX_VALUE, it wraps around to the wrong sign, and the sort comes out wrong with no error message. Integer.compare compares the two values directly and cannot overflow. (Source: Bloch, Effective Java, Item 14.)
Two keys, one of them a double
An Employee sorts by type, then by salary (a double). A double uses Double.compare, because casting a decimal difference to an int throws the decimal away: (int)(60000.75 - 60000.25) is 0, which would report two different salaries as equal.
public abstract class Employee implements Comparable<Employee>
{
protected double salary;
public String getType() { return this.getClass().getSimpleName(); }
@Override
public int compareTo(final Employee other)
{
int byType = this.getType().compareTo(other.getType());
if (byType != 0)
{
return byType;
}
return Double.compare(this.salary, other.salary);
}
}
Three keys, including a composed object
A Book sorts by publisher, then title, then isbn. The publisher is its own object with its own compareTo, so you delegate to it exactly like a String.
public class Book implements Comparable<Book>
{
private Publisher pub; // Publisher implements Comparable<Publisher>
private String title, isbn;
@Override
public int compareTo(final Book other)
{
int byPub = this.pub.compareTo(other.pub);
if (byPub != 0)
{
return byPub;
}
int byTitle = this.title.compareTo(other.title);
if (byTitle != 0)
{
return byTitle;
}
return this.isbn.compareTo(other.isbn); // isbn is unique, so this is a total order
}
}
Two, three, or five keys, the shape does not change: compare, guard, compare, guard, and return the last one plain.
Tracing a Tie so You Trust It
Sort two authors: Adams, Ken and Adams, Amy. The result should be Adams, Amy before Adams, Ken, because the last names match and Amy precedes Ken.
this = Author(first="Ken", last="Adams")
other = Author(first="Amy", last="Adams")
byLast = "Adams".compareTo("Adams") = 0 -> tie, do not return, fall through
return "Ken".compareTo("Amy") = positive -> this (Ken) sorts after other (Amy)
A positive final result means this (Ken) comes after other (Amy), so Amy is placed first. The prediction holds. Remove the early-return guard and a pair with different last names would ignore the last name entirely and order by first name alone.
Descending Order, When You Need It
Everything above sorts ascending (smallest or earliest first). To reverse one key, swap the two arguments for that key only.
// ascending by games played
return Integer.compare(this.gamesPlayed, other.gamesPlayed);
// descending by games played (most first): swap the arguments
return Integer.compare(other.gamesPlayed, this.gamesPlayed);
The swap flips the sign of that comparison, which flips the order. Reverse only the keys the instructions ask to reverse; leave the rest as they are.
Quick check
Check your understanding
An assignment says: sort Student by lastName, then firstName, then studentId. How many early-return guards does the compareTo body need?
A Short Recipe to Keep Nearby
- List the fields that decide the order, most important first. Read the instructions; if they are silent, ask what a person would do.
- Write one comparison per field, top to bottom.
- Use
field.compareTo(other.field)for a String, another Comparable object, or an enum. UseInteger.compareorDouble.comparefor a number. Never subtract. - After every field except the last, add
if (result != 0) return result;. - Return the last comparison directly. Make that last field unique when you can.
Common Misconceptions
Misconception 1: leaving out the early return
Wrong mental model: Computing all the comparisons and returning the last one is the same as a cascade.
Why it breaks: A method returns one value. If the first key is computed but not guarded with if (result != 0) return result;, control falls through to the last line, which is the only value returned. The objects are ordered by the last field alone.
How to correct: After every field except the last, add the guard. Return the final comparison directly.
Source: Bloch, Effective Java, Item 14.
Misconception 2: subtracting numbers
Wrong mental model:
return this.x - other.xis a correct, compact comparison for an int field.
Why it breaks: When the two values are far apart near the int boundary, the subtraction overflows and returns the wrong sign. The sort is silently wrong.
How to correct: Use Integer.compare(this.x, other.x) for an int and Double.compare(this.x, other.x) for a double. Neither uses arithmetic, so neither can overflow.
Source: Bloch, Effective Java, Item 14.
Practice
Level 1
A City class has a single field String name. Write the complete compareTo so Arrays.sort(cities) orders cities alphabetically by name.
Show answer
@Override
public int compareTo(final City other)
{
return this.name.compareTo(other.name);
}
One key, one delegated line.
Level 2
A Product class has String category and double price. Sort by category alphabetically, then by price from low to high. Write the body.
Thought process
Category is a String (delegate). Price is a double (Double.compare). One guard between them.
Show answer
@Override
public int compareTo(final Product other)
{
int byCategory = this.category.compareTo(other.category);
if (byCategory != 0)
{
return byCategory;
}
return Double.compare(this.price, other.price);
}
Level 3
A Match class has String tournament, int round, and String player. Sort by tournament alphabetically, then by round from highest to lowest, then by player alphabetically.
Thought process
Three keys, so two guards. Round is descending, so its arguments are swapped; the other two stay ascending.
Show answer
@Override
public int compareTo(final Match other)
{
int byTournament = this.tournament.compareTo(other.tournament);
if (byTournament != 0)
{
return byTournament;
}
int byRound = Integer.compare(other.round, this.round); // reversed for highest first
if (byRound != 0)
{
return byRound;
}
return this.player.compareTo(other.player);
}
Connections
Looking back: Natural Order Is implements Comparable<T> covers the class header and the single-key body this lesson extends. The sign rule every line here returns is covered in compare Returns a Sign, Not a Magnitude.
Looking ahead: When one built-in order is not enough and you need a second, swappable order, that is a Comparator, covered in Alternate Orders Are Comparator<T>. The overflow trap the number keys avoid has its own treatment in The Subtraction Trick and Why It Overflows.
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
A class sorts by two String fields, lastName then firstName. A student writes: int a = this.lastName.compareTo(other.lastName); int b = this.firstName.compareTo(other.firstName); return b; What is the bug?
Which line correctly compares an int field horsePower inside compareTo, ascending?
An Author sorts by last then first. this = Author(first=”Amy”, last=”Adams”); other = Author(first=”Ken”, last=”Adams”). What does this.compareTo(other) return, and who sorts first?
To sort by round from highest to lowest inside a multi-key compareTo, what do you change on that one key?