compareTo and compare, worked on a scoreboard
A Score class gets a natural order with compareTo, and a second order from a separate class with compare.
The 2 orders
A Score holds a player's initials and a number of points.
| Order | Where it is written | Rule |
|---|---|---|
| the class's own order (its natural order) | compareTo, inside Score, which implements Comparable<Score> | more points first; on equal points, initials from A to Z |
| a second order | compare, inside a separate class ByInitials, which implements Comparator<Score> | initials from A to Z, points ignored |
Both methods answer the same question with the sign of an int.
| Call | Negative | 0 | Positive |
|---|---|---|---|
a.compareTo(b) | a goes before b | a tie | a goes after b |
c.compare(a, b) | a goes before b | a tie | a goes after b |
The natural order
public class Score implements Comparable<Score>
{
private String initials;
private int points;
public Score(String initials, int points)
{
this.initials = initials;
this.points = points;
}
public String getInitials()
{
return this.initials;
}
public int getPoints()
{
return this.points;
}
@Override
public int compareTo(Score other)
{
int byPoints = Integer.compare(other.points, this.points);
if (byPoints != 0)
{
return byPoints;
}
return this.initials.compareTo(other.initials);
}
@Override
public String toString()
{
return this.initials + " " + this.points;
}
}
| Line | Why it is written that way |
|---|---|
Integer.compare(other.points, this.points) | Integer.compare(x, y) returns a negative number when x < y, 0 when they are equal, and a positive number when x > y. Putting other first flips the order, so more points comes first. |
if (byPoints != 0) | The points decide on their own unless they tie. Only a tie reaches the next key. |
this.initials.compareTo(other.initials) | String has its own compareTo, which orders by character codes. this first keeps A to Z. |
other.points | A private field of another Score is reachable here, because the code is inside the Score class. |
The sort, and 4 sample comparisons
Arrays.sort(board) calls compareTo on pairs and arranges the array by the signs.
import java.util.Arrays;
// inside main
Score[] board = {
new Score("JD", 90),
new Score("AB", 75),
new Score("KL", 90),
new Score("ZZ", 75),
new Score("AA", 100)
};
Arrays.sort(board);
System.out.println(Arrays.toString(board));
[AA 100, JD 90, KL 90, AB 75, ZZ 75]
| Call | Key that decides | Result |
|---|---|---|
JD 90 against AA 100 | points, 90 against 100 | 1, so JD goes after AA |
JD 90 against KL 90 | points tie; initials, "JD".compareTo("KL") | -1, so JD goes before KL |
AB 75 against JD 90 | points, 75 against 90 | 1, so AB goes after JD |
KL 90 against a second KL 90 | both keys tie | 0 |
A second order, from a separate class
A class can have only 1 compareTo. Any other order goes in a separate class that implements Comparator (a comparator). Its method takes both objects as parameters.
import java.util.Comparator;
public class ByInitials implements Comparator<Score>
{
@Override
public int compare(Score a, Score b)
{
return a.getInitials().compareTo(b.getInitials());
}
}
compareTo | compare | |
|---|---|---|
| interface on the class line | Comparable<Score>, on Score itself | Comparator<Score>, on a separate class |
| parameters | 1, the other object; this is the first | 2, both objects |
| reaching the fields | directly, because the code is inside Score | through getters, because the code is in another class |
| sorting with it | Arrays.sort(board) | Arrays.sort(board, new ByInitials()) |
Sorting the same 5 scores with new ByInitials() prints:
[AA 100, AB 75, JD 90, KL 90, ZZ 75]
The cases, one call at a time
| Call | Returns | Why |
|---|---|---|
new Score("JD", 90).compareTo(new Score("KL", 90)) | -1 | points tie, and J is 1 before K |
new Score("JD", 90).compareTo(new Score("AA", 100)) | 1 | fewer points goes after |
new Score("KL", 90).compareTo(new Score("KL", 90)) | 0 | every key ties |
new ByInitials().compare(new Score("ZZ", 75), new Score("AA", 100)) | 25 | Z is 25 letters after A; only the sign matters |
return other.points - this.points; gives the right sign for small numbers. When the difference is too big for an int, the result wraps around to the other sign (integer overflow). With this.points equal to 1 and other.points equal to Integer.MIN_VALUE, the subtraction returns 2147483647, a positive number, and Integer.compare(other.points, this.points) returns -1. Integer.compare never overflows.
Drill
4 questions. Work each one on paper before you tap.
Level-up check
1. Predict the output. The board holds JD 90, AB 75, KL 90, ZZ 75, AA 100, in that order. What does Arrays.sort(board) followed by Arrays.toString(board) print?
Points decide first, highest first, and initials break the ties from A to Z. The first choice is the order with the points comparison written the other way round. The third is the ByInitials order. The fourth breaks the ties from Z to A.
2. The sign. What does new Score("AB", 75).compareTo(new Score("JD", 90)) return?
The points differ, so they decide and the initials are never read. Fewer points goes after, so the sign is positive. The first choice reads the second key when the first key already decided.
3. Spot the bug. Someone writes the first line of compareTo as int byPoints = Integer.compare(this.points, other.points); and leaves the rest alone. What does the sort from question 1 print?
Swapping the arguments flips the sign, so fewer points now comes first. The tie breaker was not touched, so ties still run A to Z. The second choice flips the tie breaker too.
4. Which method. A driver writes Arrays.sort(board, new ByInitials());. Which method does the sort call?
When a comparator is passed in, the sort uses only that comparator. compareTo is used when no comparator is given.
Write it out
Not scored. Type it from memory, then compare. The box is scratch space and is not saved.
Rewrite Score's compareTo so that initials decide first, from A to Z, and on equal initials more points comes first.
@Override
public int compareTo(Score other)
{
int byInitials = this.initials.compareTo(other.initials);
if (byInitials != 0)
{
return byInitials;
}
return Integer.compare(other.points, this.points);
}
On a board holding JD 60, AB 75, JD 90, ZZ 75, AA 100, Arrays.sort then prints [AA 100, AB 75, JD 90, JD 60, ZZ 75]. The 2 JD scores tie on the first key, so the points decide between them.
Your checklist
Tick these when they are true.
Could you write this cold?
Blank file, no notes, 4 hours on a clock, and only the Java API documentation open. Say yes only when that is true. You can take it back at any time.
- You change only the lines between
//startMethodand//endMethod, and you keep each method header as given. - A private helper method or field of your own between the markers is fine.
- No new imports.
The official site's Basic Strategies page says: You may receive points for compiling code but you WON’T receive points for code that does not compile.
The compareTo problem in each practice APE (on Student in Practice exams 1 and 2, on Luggage in Practice exam 3) and the StudentComparator problem in Practice exam 2 use these moves.
If a piece of this is shaky
- Implementing Comparable, for the contract and the multi key cascade.
- Order is a sign, if the negative and positive results still feel arbitrary.
- An interface is a promise, if you are not sure what
implementsbuys you. - Natural order is
implements Comparableand the result is a sign only. - The multi key
compareTobody. - The subtraction trick and why it overflows, then the fix.
- Consistency with equals, for what a result of 0 promises.
Further reading
- Comparable (Java SE 19 & JDK 19). The contract, including the sentence about the sign.
- Comparator (Java SE 19 & JDK 19). The 2-parameter
compareand its contract. - String.compareTo (Java SE 19 & JDK 19). The first differing character, and the fall back to the difference in lengths.
- Integer.compare (Java SE 19 & JDK 19). What it returns for each case.
- Arrays.sort (Java SE 19 & JDK 19). The sort that reads
compareTo, and the sentence saying it is stable. - Implementing an Interface (The Java Tutorials). Writing a class that keeps an interface's promise.
- Object Ordering (The Java Tutorials). Natural order and comparators on a class with several keys.
- Total order (Wikipedia). Where the rules in the contract come from.
- Integer overflow (Wikipedia). Why subtracting to compare can return the wrong sign.