Tuesday's quiz asks you to write two methods: a multi-key compareTo and a copy constructor. It is mechanical and should be roughly exactly the same each time you do it, in a similar way that toString, equals, and hashCode are mechanical and roughly exactly the same each time you do it.
1. Multi-key compareTo
The contract of compareTo is that it returns a sign: a negative number, a positive number, or zero.
Wait, so it does not have to be exactly 1 or -1?
Correct. It only has to be negative, positive, or zero. For example, here is what actually prints when calling compareTo on strings:
"cat".compareTo("car") = 2 -> positive
It returns 2, not 1. The exact number does not matter, only the sign matters. Never assume it subtracts or returns exactly 1 or -1; that is not part of the contract.
What is the pattern for a multi-key compareTo?
Suppose we have a Player class that implements Comparable<Player>. We want to sort by name (a String), and then break ties with jerseyNumber (an int).
Here is the mechanical pattern you will write every time:
@Override
public int compareTo(Player other) {
if (other == null) {
throw new IllegalArgumentException("cannot compare to a null Player");
}
int value = this.name.compareTo(other.name);
if (value == 0) {
value = Integer.compare(this.jerseyNumber, other.jerseyNumber);
}
return value;
}
Why use Integer.compare instead of just subtracting the ints?
Subtraction can overflow. If you subtract a very large negative integer from a large positive integer, the result might wrap around and give the wrong sign. Integer.compare is safe and handles those edges correctly.
What if I need a third tie-breaker?
You chain another if (value == 0) block before the return. For example:
if (value == 0) {
value = Integer.compare(this.thirdField, other.thirdField);
}
2. Copy Constructors
A copy constructor builds a new, separate object using the data from an existing one.
How do I write a copy constructor for a single object?
public Player(Player other) {
if (other == null) {
throw new IllegalArgumentException("cannot copy a null Player");
}
this.name = other.name;
this.jerseyNumber = other.jerseyNumber;
}
Wait, why is it safe to share the String name?
Strings in Java are immutable. Because you cannot change a String once it is made, it is perfectly safe to share the same String object across multiple players. Primitives like int are copied by value. Mutable objects, however, must be copied into fresh storage.
How do I write a copy constructor if the field is an array of objects?
This is the question on the quiz. If a Team has a Player[] players array, you must allocate a new array of the exact same size, and then loop to deeply copy every element.
public Team(Team other) {
if (other == null) {
throw new IllegalArgumentException("cannot copy a null Team");
}
this.players = new Player[other.players.length];
for (int i = 0; i < other.players.length; i++) {
Player p = other.players[i];
if (p == null) {
this.players[i] = null;
} else {
this.players[i] = new Player(p);
}
}
}
What happens if I just use .clone() on the array?
That is a trap. Calling .clone() on an array creates a shallow copy. It builds a new array, but populates it with the exact same object references as the old array. The arrays are separate, but the players inside them are shared.
Player in memory. We changed one reference to Alice #99, and now both arrays see it.You must write the loop to create deep copies.
What if the field is an ArrayList instead of an array?
You can often use the copy constructor of ArrayList itself:
this.list = new ArrayList<>(other.list);
However, if the list contains mutable objects, this is not enough; you still need to write a loop to do an element-by-element deep copy.
3. Readiness Check
You have got this. It is the same moves every time.