Comparable and Cloneable in Java — exercises with books, players and a cloned squad (and the subtraction that put the richest account first)
I was sorting bank accounts by balance and wrote, without a care, return this.balance - other.balance; inside compareTo. With the class data it worked perfectly: 100, 250, 4000… The day I tried a huge balance and a huge overdraft, the two-billion account came out first in a list that was supposed to go from lowest to highest. No exception, no message, nothing: Java sorted wrongly in perfect good faith, because the subtraction had overflowed and my compareTo was lying.
It’s the kind of bug you don’t learn by reading: you learn it by breaking it. Today we practise with three programs of increasing difficulty, three fragments to debug (one of them is that subtraction) and a cheat sheet to take with you.
These exercises use the Comparable and Cloneable theory and review the practice. You can do them without those, but it will be harder.
Table of Contents
How to use this article
- Read the statement of one level and don’t look at the solution yet.
- Write your version, compile it and compare your output with the expected one.
- If you get stuck, look only at the 🔵 hint and try again.
- When you finish, compare with the solution and notice why it’s done that way, not just that it gives the same output.
No exercise uses Scanner. The data is fixed in the code so your output is identical to the expected one and you can check it at a glance.
Level 1 · Basic: books sorted by pages
Statement. Create a Book class with a title and a number of pages (int), with its getters and a toString that returns, for example, Platero and I (160 pp.). Make Book implement Comparable<Book> with this natural order: from fewest to most pages.
In an N1 class with main, create an array with these four books, sort it with Arrays.sort and print it:
- Don Quixote, 1376 pages
- Platero and I, 160 pages
- La Celestina, 280 pages
- Fortunata and Jacinta, 1008 pages
Then show the shortest and the longest (without sorting again: use the positions of the already sorted array).
Expected output:
From fewest to most pages: Platero and I (160 pp.) La Celestina (280 pp.) Fortunata and Jacinta (1008 pp.) Don Quixote (1376 pp.) Shortest: Platero and I (160 pp.) Longest: Don Quixote (1376 pp.)
Once the array is sorted, the shortest is at position 0 and the longest at shelf.length - 1. And to compare integers, no subtractions: Integer.compare.
Level 2 · Intermediate: a league table
Statement. Create a Player class with a name, points and games played (all three as final attributes, with getters) and a toString in the format Lía 150 pts in 10 games. Don’t make it Comparable: the table criterion will be a separate Comparator.
In N2, declare a constant static final Comparator<Player> RANKING that sorts by:
- Most points first.
- On a tie, fewest games played first (whoever got there in fewer games goes ahead).
- If they still tie, by name.
With these five players in a list, sort it and print the numbered top 3 and the last one:
| Name | Points | Games |
|---|---|---|
| Noa | 120 | 10 |
| Iker | 150 | 12 |
| Lía | 150 | 10 |
| Hugo | 90 | 8 |
| Aitana | 120 | 10 |
Expected output:
Top 3: 1. Lía 150 pts in 10 games 2. Iker 150 pts in 12 games 3. Aitana 120 pts in 10 games Last: Hugo 90 pts in 8 games
comparingInt(...).reversed() reverses only what comes before the reversed(). Put it right after the first criterion and add the others with thenComparing...: that way the points go from most to fewest but the games go from fewest to most.
Level 3 · Challenge: a squad that really gets cloned
Statement. A team has a squad of players and a stadium. We want to be able to make a deep copy of the squad to try out signings without touching the original. Create three classes, **all Cloneable**, with a public clone() and no checked exception:
Stadium: name and capacity (int, withsetCapacity).toString:The Island (5000).Player: name and points (int, with anadd(int)method).toString:Noa:10.Squad: team, list of players and stadium; methodssign(Player),getPlayers()andgetStadium().toString:Tigers [Noa:10, Iker:12] in The Island (5000).
Squad‘s clone() must be deep: the copy has its own list, its own players and its own stadium.
In N3, create the squad “Tigers” with the stadium “The Island” (5000) and the players Noa (10) and Iker (12). Clone it and, in the copy only: sign Lía (7), add 100 points to the first player and change the capacity to 9000. Print both squads and whether the first player of each is the same object.
Expected output:
Original: Tigers [Noa:10, Iker:12] in The Island (5000) Copy: Tigers [Noa:110, Iker:12, Lía:7] in The Island (9000) Same player at position 0? false
super.clone() gives you a Squad with the same list and the same stadium. Replace the list field with a new list and, inside a loop, add j.clone() for each player. The stadium, with stadium.clone().
What’s wrong here?
Three fragments, each with one bug. Before reading the explanation, do two things: predict whether it compiles and, if it does, what it prints (or what happens when you run it).
Fragment 1
Account.java
public class Account implements Comparable<Account> {
private final String holder;
private final int balance; // in cents
public Account(String holder, int balance) {
this.holder = holder;
this.balance = balance;
}
@Override
public int compareTo(Account other) {
return this.balance - other.balance;
}
@Override
public String toString() { return holder + ": " + balance; }
}
F1.java
import java.util.Arrays;
public class F1 {
public static void main(String[] args) {
Account[] accounts = {
new Account("Ana", 2_000_000_000),
new Account("Luis", -2_000_000_000),
new Account("Marta", 5)
};
Arrays.sort(accounts);
System.out.println(Arrays.toString(accounts));
}
}
Think about it before going on: does it compile? What does it print? The accounts should come out from lowest to highest balance.
Explanation. It compiles and throws no exception. But the result is this:
[Ana: 2000000000, Luis: -2000000000, Marta: 5]
The order is wrong: Ana (two billion) comes out first and Luis (minus two billion) in the middle. The subtraction is to blame. Comparing Ana with Luis calculates 2 000 000 000 - (-2 000 000 000) = 4 000 000 000, which **doesn’t fit in an int** (the maximum is 2 147 483 647) and overflows into a negative number. compareTo tells Java that Ana goes before Luis.
return this.x - other.x in an integer compareTo. It works with small values and fails silently with large values or values of opposite sign.
Fix. Let Java do the comparing, without subtracting:
@Override
public int compareTo(Account other) {
return Integer.compare(this.balance, other.balance);
}
[Luis: -2000000000, Marta: 5, Ana: 2000000000]
Integer.compare(a, b) (or Double.compare) exists precisely for this. It’s the same as the subtraction, but without the overflow.
Fragment 2
Player.java
public class Player implements Comparable<Player> {
private final String name;
private final int points;
public Player(String name, int points) {
this.name = name;
this.points = points;
}
@Override
public int compareTo(Player other) {
return Integer.compare(this.points, other.points);
}
@Override
public String toString() { return name + " " + points; }
}
F2.java
import java.util.Arrays;
public class F2 {
public static void main(String[] args) {
Player[] squad = new Player[4]; // we haven't signed the fourth one yet
squad[0] = new Player("Noa", 120);
squad[1] = new Player("Iker", 150);
squad[2] = new Player("Lía", 90);
Arrays.sort(squad);
System.out.println(Arrays.toString(squad));
}
}
Does it compile? What does it print? What happens with the position that hasn’t been filled in?
Explanation. It compiles fine. But new Player[4] creates an array of four **null references**, and we’ve only filled three. Arrays.sort tries to compare all the elements, and compares with one that doesn’t exist:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "pivot" is null at java.base/java.util.ComparableTimSort.binarySort(ComparableTimSort.java:262) at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:189) at java.base/java.util.Arrays.sort(Arrays.java:1042) at F2.main(F2.java:10)
NullPointerException inside Arrays.sort or Collections.sort. The natural order calls compareTo on the elements, and a null has no methods. The failure shows up far from where you put the null (here, in the new Player[4]).
Fix. Two options: sort only the filled part, or tell Java what to do with the nulls. The second one, with a Comparator that sends them to the end:
import java.util.Arrays;
import java.util.Comparator;
public class F2Fix {
public static void main(String[] args) {
Player[] squad = new Player[4];
squad[0] = new Player("Noa", 120);
squad[1] = new Player("Iker", 150);
squad[2] = new Player("Lía", 90);
Arrays.sort(squad, Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(Arrays.toString(squad));
}
}
[Lía 90, Noa 120, Iker 150, null]
Comparator.nullsLast(...) and nullsFirst(...) wrap any comparator so it knows what to do with nulls. But the best fix is usually for the array to have no gaps.
Fragment 3
Player.java
public class Player implements Cloneable {
protected String name;
protected int points;
public Player(String name, int points) {
this.name = name;
this.points = points;
}
@Override
public Player clone() {
return new Player(name, points); // "make one just like it", right?
}
@Override
public String toString() { return name + " " + points; }
}
Goalkeeper.java
public class Goalkeeper extends Player {
private int saves;
public Goalkeeper(String name, int points, int saves) {
super(name, points);
this.saves = saves;
}
@Override
public Goalkeeper clone() {
Goalkeeper copy = (Goalkeeper) super.clone();
copy.saves = this.saves;
return copy;
}
@Override
public String toString() { return super.toString() + " and " + saves + " saves"; }
}
F3.java
public class F3 {
public static void main(String[] args) {
Goalkeeper original = new Goalkeeper("Noa", 120, 35);
Goalkeeper copy = original.clone();
System.out.println(copy);
}
}
*Does it compile? What does it print? Look at how the copy is created in Player.clone().*
Explanation. It compiles. But Player.clone() doesn’t use super.clone(): it builds the copy with new Player(...), so it always returns a plain Player, even if the object being cloned is a Goalkeeper. When Goalkeeper.clone() tries to convert that result to Goalkeeper, the cast fails:
Exception in thread "main" java.lang.ClassCastException: class Player cannot be cast to class Goalkeeper (Player and Goalkeeper are in unnamed module of loader 'app') at Goalkeeper.clone(Goalkeeper.java:11) at F3.main(F3.java:4)
ClassCastException in a subclass’s clone(). The superclass creates the copy with new instead of super.clone(), and the copy loses its real type.
Fix. In the superclass, use super.clone(), which creates an object of the real type of the one being cloned:
@Override
public Player clone() {
try {
return (Player) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
Noa 120 and 35 saves
Golden rule: inside any clone(), the first line is super.clone(). If you see a new there, be suspicious.
Solutions
Look at this section only once you’ve tried the exercise. If your code is different but gives the same output and respects the contracts, it’s also a valid solution.
Level 1 solution
Book.java
public class Book implements Comparable<Book> {
private final String title;
private final int pages;
public Book(String title, int pages) {
this.title = title;
this.pages = pages;
}
public String getTitle() { return title; }
public int getPages() { return pages; }
// Natural order: from fewest to most pages
@Override
public int compareTo(Book other) {
return Integer.compare(this.pages, other.pages);
}
@Override
public String toString() {
return title + " (" + pages + " pp.)";
}
}
N1.java
import java.util.Arrays;
public class N1 {
public static void main(String[] args) {
Book[] shelf = {
new Book("Don Quixote", 1376),
new Book("Platero and I", 160),
new Book("La Celestina", 280),
new Book("Fortunata and Jacinta", 1008)
};
Arrays.sort(shelf);
System.out.println("From fewest to most pages:");
for (Book book : shelf) {
System.out.println(" " + book);
}
System.out.println("Shortest: " + shelf[0]);
System.out.println("Longest: " + shelf[shelf.length - 1]);
}
}
Why it’s done this way:
Integer.compare(this.pages, other.pages)instead of a subtraction: it gives the same result with no risk of overflow.Arrays.sort(shelf)works becauseBookisComparable: without that, we’d have theClassCastExceptionfrom the theory.- The shortest and the longest come for free from positions
0andlength - 1. Sorting once is much cheaper than looking for the minimum and the maximum separately.
Level 2 solution
Player.java
public class Player {
private final String name;
private final int points;
private final int games;
public Player(String name, int points, int games) {
this.name = name;
this.points = points;
this.games = games;
}
public String getName() { return name; }
public int getPoints() { return points; }
public int getGames() { return games; }
@Override
public String toString() {
return name + " " + points + " pts in " + games + " games";
}
}
N2.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class N2 {
// More points first; on a tie, fewer games played; if still tied, by name
static final Comparator<Player> RANKING = Comparator
.comparingInt(Player::getPoints).reversed()
.thenComparingInt(Player::getGames)
.thenComparing(Player::getName);
public static void main(String[] args) {
List<Player> squad = new ArrayList<>(List.of(
new Player("Noa", 120, 10), new Player("Iker", 150, 12),
new Player("Lía", 150, 10), new Player("Hugo", 90, 8),
new Player("Aitana", 120, 10)));
squad.sort(RANKING);
System.out.println("Top 3:");
for (int i = 0; i < 3; i++) {
System.out.println(" " + (i + 1) + ". " + squad.get(i));
}
System.out.println("Last: " + squad.get(squad.size() - 1));
}
}
Why it’s done this way:
Playeris notComparable: a league table is a specific criterion of a league, not “the” order of a player. That’s why it lives in aComparator.comparingInt(Player::getPoints).reversed()reverses only the points. The games are added afterwards withthenComparingInt, so they go from fewest to most.- Lía and Iker tie at 150 points; Lía goes first for playing fewer games (10 versus 12). Noa and Aitana tie at 120 with the same number of games, and the name wins:
AitanabeforeNoa… but only Aitana makes the top 3. Change the data and check that the final tie-breaker works. - The constant is
static final: a comparator has no state, so it can be reused and shared without risk.
Level 3 solution
Stadium.java
public class Stadium implements Cloneable {
private String name;
private int capacity;
public Stadium(String name, int capacity) {
this.name = name;
this.capacity = capacity;
}
public void setCapacity(int capacity) { this.capacity = capacity; }
@Override
public Stadium clone() {
try {
return (Stadium) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() { return name + " (" + capacity + ")"; }
}
Player.java
public class Player implements Cloneable {
private final String name;
private int points;
public Player(String name, int points) {
this.name = name;
this.points = points;
}
public void add(int p) { points += p; }
@Override
public Player clone() {
try {
return (Player) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() { return name + ":" + points; }
}
Squad.java
import java.util.ArrayList;
import java.util.List;
public class Squad implements Cloneable {
private String team;
private List<Player> players = new ArrayList<>();
private Stadium stadium;
public Squad(String team, Stadium stadium) {
this.team = team;
this.stadium = stadium;
}
public void sign(Player j) { players.add(j); }
public List<Player> getPlayers() { return players; }
public Stadium getStadium() { return stadium; }
@Override
public Squad clone() {
try {
Squad copy = (Squad) super.clone();
// The new list must hold COPIES of the players, not the same objects
copy.players = new ArrayList<>();
for (Player j : this.players) {
copy.players.add(j.clone());
}
copy.stadium = this.stadium.clone();
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() {
return team + " " + players + " in " + stadium;
}
}
N3.java
public class N3 {
public static void main(String[] args) {
Squad original = new Squad("Tigers", new Stadium("The Island", 5000));
original.sign(new Player("Noa", 10));
original.sign(new Player("Iker", 12));
Squad copy = original.clone();
copy.sign(new Player("Lía", 7));
copy.getPlayers().get(0).add(100);
copy.getStadium().setCapacity(9000);
System.out.println("Original: " + original);
System.out.println("Copy: " + copy);
System.out.println("Same player at position 0? "
+ (original.getPlayers().get(0) == copy.getPlayers().get(0)));
}
}
Why it’s done this way:
StadiumandPlayeronly have primitives andStrings, so theirclone()is the simplest one:super.clone()and nothing else.- In
Squad.clone(),super.clone()copies the reference to the list and to the stadium. That’s why we replace them with a new list and a cloned stadium. If we had only copied the list withnew ArrayList<>(this.players), the list would be new but the players would still be the same objects. - The last line of the output proves it:
falsemeans the first player of the copy is another object. That’s why adding 100 points to them hasn’t touched Noa in the original. - The
try/catchis inside eachclone()so whoever calls it doesn’t have to handle the checked exception. It’s the recipe from the theory.
Watch it step by step in Python Tutor
The subtraction bug is a good candidate to watch live, because it happens inside a single line. Go to pythontutor.com/java.html, choose Java and paste this program, which compares the same two numbers in both ways:
public class Tutor {
static int compareBadly(int a, int b) {
return a - b;
}
static int compareWell(int a, int b) {
return Integer.compare(a, b);
}
public static void main(String[] args) {
int big = 2_000_000_000;
int small = -2_000_000_000;
int bad = compareBadly(big, small);
int good = compareWell(big, small);
System.out.println("a - b = " + bad);
System.out.println("Integer.compare = " + good);
}
}
a - b = -294967296 Integer.compare = 1
What to watch, step by step:
- When
mainstarts, look atbig(2 000 000 000) andsmall(-2 000 000 000): both fit perfectly in anint. - Step into
compareBadly(a, b). Before thereturnruns, work out the subtraction yourself: it should be 4 000 000 000, a positive number. - When you get back to
main, look at the value ofbad: it’s negative (-294 967 296). Theinthas overflowed and Java gave no warning. - Then
compareWellreturns1, which is the correct result:biggoes aftersmall. Compare the two signs and you’ll understand the bug in fragment 1.
Cheat sheet: Comparable and Cloneable
The essentials in 30 seconds
- **
Comparable<T>**:compareTodefines the class’s natural order. Negative = before, 0 = equivalent, positive = after. - **
Comparator<T>: external** criterion; you can have as many as you want and chain them. - Never subtract to compare: use
Integer.compare,Double.compareorcompareTo. - An order that gives
0for different elements loses data inTreeSetandTreeMap. - **
clone()**:implements Cloneable+public+super.clone()+try/catch. super.clone()is shallow: whatever is mutable (lists, arrays, other objects) must be duplicated by hand.
Syntax
| I want to… | Code | Result |
|---|---|---|
| Natural order for my class | class A implements Comparable<A> { public int compareTo(A o) {…} } | sort(list) works |
| Compare integers | Integer.compare(this.x, o.x) | No overflow |
| Compare with a tie-breaker | if (c != 0) return c; return this.n.compareTo(o.n); | Never 0 for different ones |
| Sort with an external criterion | list.sort(comparator) | Comparable not needed |
| Criterion by one field | Comparator.comparing(A::getN) | Ascending order |
| Reverse | Comparator.comparingInt(A::getX).reversed() | Descending |
| Chain tie-breakers | ….thenComparing(A::getN) | Second criterion |
Natural order with null last | Comparator.nullsLast(Comparator.naturalOrder()) | No NullPointerException |
| Sorted set | new TreeSet<>(comparator) | Looks only at the comparator |
| Copy an object | A copy = original.clone(); | Needs a public clone() |
| Deep copy | copy.list = new ArrayList<>(this.list); | Its own list |
Templates for the exercises
**compareTo without subtractions** (Level 1):
@Override
public int compareTo(Book other) {
return Integer.compare(this.pages, other.pages);
}
Chained Comparator (Level 2):
static final Comparator<Player> RANKING = Comparator
.comparingInt(Player::getPoints).reversed() // only reverses the points
.thenComparingInt(Player::getGames)
.thenComparing(Player::getName);
**Deep clone()** (Level 3):
@Override
public Squad clone() {
try {
Squad copy = (Squad) super.clone();
copy.players = new ArrayList<>();
for (Player j : this.players) {
copy.players.add(j.clone()); // each player copied too
}
copy.stadium = this.stadium.clone();
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
The mistakes that repeat the most
| Symptom | Likely cause |
|---|---|
ClassCastException … cannot be cast to class java.lang.Comparable | The class doesn’t implement Comparable and you sort it without a Comparator |
| “is not abstract and does not override abstract method compareTo” | implements Comparable without writing compareTo |
| Odd order, no exception, with very large values | compareTo with a subtraction that overflows |
Elements missing from a TreeSet/TreeMap | The criterion gives 0 for elements you consider different |
NullPointerException inside sort | There’s a null in the list or the array |
CloneNotSupportedException | implements Cloneable is missing |
| “clone() has protected access in Object” | clone() wasn’t redefined as public |
| The copy changes the original | Shallow copy: shared list or object |
ClassCastException in a subclass’s clone() | The superclass does new instead of super.clone() |
Before you hand it in, ask yourself
- Does my
compareTouse **Integer.compare/Double.compare/compareTo** and not a subtraction? - Does my ordering criterion tell apart the elements that are different (is there a tie-breaker)?
- Does my
clone()start with **super.clone()** and never withnew? - If there are lists, arrays or mutable objects inside, have I duplicated them in the copy?
If you want to review the topic from the basics, go back to the Comparable and Cloneable theory.
Keep goingAll Java (en) articlesDid this article help you? Here is how you can support SergioLearns →
