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.

How to use this article

  1. Read the statement of one level and don’t look at the solution yet.
  2. Write your version, compile it and compare your output with the expected one.
  3. If you get stuck, look only at the 🔵 hint and try again.
  4. 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:

  1. Most points first.
  2. On a tie, fewest games played first (whoever got there in fewer games goes ahead).
  3. 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:

NamePointsGames
Noa12010
Iker15012
Lía15010
Hugo908
Aitana12010

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, with setCapacity). toString: The Island (5000).
  • Player: name and points (int, with an add(int) method). toString: Noa:10.
  • Squad: team, list of players and stadium; methods sign(Player), getPlayers() and getStadium(). 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 because Book is Comparable: without that, we’d have the ClassCastException from the theory.
  • The shortest and the longest come for free from positions 0 and length - 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:

  • Player is not Comparable: a league table is a specific criterion of a league, not “the” order of a player. That’s why it lives in a Comparator.
  • comparingInt(Player::getPoints).reversed() reverses only the points. The games are added afterwards with thenComparingInt, 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: Aitana before Noa… 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:

  • Stadium and Player only have primitives and Strings, so their clone() 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 with new 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: false means 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/catch is inside each clone() 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:

  1. When main starts, look at big (2 000 000 000) and small (-2 000 000 000): both fit perfectly in an int.
  2. Step into compareBadly(a, b). Before the return runs, work out the subtraction yourself: it should be 4 000 000 000, a positive number.
  3. When you get back to main, look at the value of bad: it’s negative (-294 967 296). The int has overflowed and Java gave no warning.
  4. Then compareWell returns 1, which is the correct result: big goes after small. 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>**: compareTo defines 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.compare or compareTo.
  • An order that gives 0 for different elements loses data in TreeSet and TreeMap.
  • **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…CodeResult
Natural order for my classclass A implements Comparable<A> { public int compareTo(A o) {…} }sort(list) works
Compare integersInteger.compare(this.x, o.x)No overflow
Compare with a tie-breakerif (c != 0) return c; return this.n.compareTo(o.n);Never 0 for different ones
Sort with an external criterionlist.sort(comparator)Comparable not needed
Criterion by one fieldComparator.comparing(A::getN)Ascending order
ReverseComparator.comparingInt(A::getX).reversed()Descending
Chain tie-breakers….thenComparing(A::getN)Second criterion
Natural order with null lastComparator.nullsLast(Comparator.naturalOrder())No NullPointerException
Sorted setnew TreeSet<>(comparator)Looks only at the comparator
Copy an objectA copy = original.clone();Needs a public clone()
Deep copycopy.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

SymptomLikely cause
ClassCastException … cannot be cast to class java.lang.ComparableThe 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 valuescompareTo with a subtraction that overflows
Elements missing from a TreeSet/TreeMapThe criterion gives 0 for elements you consider different
NullPointerException inside sortThere’s a null in the list or the array
CloneNotSupportedExceptionimplements Cloneable is missing
“clone() has protected access in Object”clone() wasn’t redefined as public
The copy changes the originalShallow 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

  1. Does my compareTo use **Integer.compare/Double.compare/compareTo** and not a subtraction?
  2. Does my ordering criterion tell apart the elements that are different (is there a tie-breaker)?
  3. Does my clone() start with **super.clone()** and never with new?
  4. 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) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *