ExerciseChallenge
Challenge
The equals that was ignored
20 minjunior1–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- equals(MyType) is an OVERLOAD; only equals(Object) is an override
- Collections call equals(Object) through the interface, so the overload never runs
- @Override turns this from a silent production bug into a compile error
- hashCode has to agree with whatever equals you actually wrote
Starter
Starter.javaOpen in playground
import java.util.*;
/**
* CHALLENGE — 20 minutes.
*
* Dedup is not deduplicating. A support ticket says the same customer gets
* the same notification several times.
*
* The class has an equals. It looks correct. A direct call to it returns
* true. The HashSet still holds duplicates.
*
* TASKS
* 1. Run it. Note that assertion (a) passes and (b) fails — the equals
* works when YOU call it and not when the collection does.
* 2. Add @Override to equals. Read the compile error. That is the bug.
* 3. Fix it, and then fix what the HashSet needs beyond equals.
* 4. In a comment: the original compiled cleanly and no tool warned. Why
* not, and what would have caught it?
*/
public class Starter {
static final class Recipient {
private final String email;
Recipient(String email) {
this.email = email;
}
/** Looks right. Is not an override. */
public boolean equals(Recipient other) {
return other != null && email.equalsIgnoreCase(other.email);
}
@Override
public String toString() {
return email;
}
}
public static void main(String[] args) {
Recipient a = new Recipient("ravi@example.com");
Recipient b = new Recipient("RAVI@example.com");
System.out.println("(a) a.equals(b) directly : " + a.equals(b));
Set<Recipient> unique = new HashSet<>();
unique.add(a);
unique.add(b);
System.out.println("(b) HashSet size : " + unique.size() + " (expected 1)");
System.out.println("(c) HashSet contains a copy : "
+ unique.contains(new Recipient("ravi@example.com")));
List<Recipient> list = new ArrayList<>(List.of(a));
System.out.println("(d) List contains a copy : "
+ list.contains(new Recipient("ravi@example.com")));
System.out.println();
System.out.println("notifications that would be sent: " + unique.size());
// Question to answer in a comment before you move on:
// (b) and (d) both fail, but for different reasons. What does a
// HashSet need that an ArrayList does not?
}
}Run it locally:
cd exercises/java/oop/overloading-vs-overriding/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Look at the parameter type of equals. What does HashSet call?
Hint 2
Add @Override to it and compile. The error is the whole diagnosis.
Hint 3
Once equals(Object) is right, the set still misbehaves. What else does a hash-based collection need?
Hint 4
Try it in an ArrayList too — contains() uses equals but not hashCode, so the two collections fail for different reasons and at different times.
Done when
- equals takes Object and carries @Override
- hashCode is consistent with equals
- The HashSet holds one element and contains() finds it
- A comment explains why the original compiled and why nothing warned
Stretch
Write the version that would have caught this in CI without anyone
remembering to add @Override: a test that puts two equal objects in a
HashSet and asserts size 1. Then say why a test that only calls
a.equals(b) directly would have passed against the broken code — which is
exactly how this bug survives review.
← Back to What is the difference between overloading and overriding?