ExerciseChallenge
Challenge
The set that grew duplicates
20 minintermediate1–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Overriding equals() without hashCode() loses entries silently
- The two methods must read the same fields — that agreement is the contract
- A record fixes it structurally; a hand-written hashCode() fixes it locally
- Nothing throws, so only a test catches this
Starter
Starter.java
import java.util.*;
/**
* Challenge: this set is supposed to hold three distinct tags. It holds five.
*
* Nothing throws. size() is simply wrong, and contains() lies. Work out why
* before you change anything, then fix it two different ways and decide which
* one you would ship.
*/
public class Starter {
static final class Tag {
private final String name;
private final String colour;
Tag(String name, String colour) {
this.name = name;
this.colour = colour;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag other)) return false;
return name.equals(other.name) && colour.equals(other.colour);
}
// TODO: something is missing here.
@Override
public String toString() {
return name + "/" + colour;
}
}
public static void main(String[] args) {
List<Tag> incoming = List.of(
new Tag("urgent", "red"),
new Tag("later", "grey"),
new Tag("urgent", "red"), // duplicate of the first
new Tag("blocked", "amber"),
new Tag("later", "grey")); // duplicate of the second
Set<Tag> distinct = new HashSet<>(incoming);
System.out.println("incoming = " + incoming.size());
System.out.println("distinct tags = " + distinct.size() + " (should be 3)");
System.out.println("contains urgent = "
+ distinct.contains(new Tag("urgent", "red")) + " (should be true)");
// Two objects equals() calls identical...
Tag a = new Tag("urgent", "red");
Tag b = new Tag("urgent", "red");
System.out.println("a.equals(b) = " + a.equals(b));
// ...that the set treats as different. Why?
System.out.println("same hash = " + (a.hashCode() == b.hashCode()));
// TODO 1: fix Tag so distinct.size() is 3 without touching this method.
//
// TODO 2: in a comment, say what would ALSO change if you converted Tag
// into a record instead. There are at least three differences, and one
// of them is about subclassing.
}
}Run it locally:
cd exercises/java/oop/hashcode-equals-contract/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
HashSet finds the bucket from the hash first. If the hashes differ, equals() is never called at all.
Hint 2
Object.hashCode() is identity-based, so two distinct instances almost always land in different buckets.
Hint 3
There are two valid fixes and they are not equivalent — one changes the class's shape, the other changes one method.
Done when
- The set holds one entry per logical tag, not one per instance
- equals() and hashCode() read exactly the same fields
- A comment explains what converting Tag to a record would also change
← Back to What is the contract between hashCode() and equals()?