Code Review · Java Collections
Java Code Review: Mutable Objects Inside HashSet
Review Java code where an object is inserted into a HashSet and then mutated, causing contains() to fail unexpectedly.
Code Review Exercise
Review the following Java code. It looks reasonable at first: create a user, add it to a HashSet, then check whether the set still contains it.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class User {
private String email;
User(String email) {
this.email = email;
}
void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}
public class Demo {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
User user = new User("alice@example.com");
users.add(user);
user.setEmail("alice@newdomain.com");
System.out.println(users.contains(user)); // false?
}
}What issues do you see before scrolling further?
Review Findings
Issue #1: Mutable field used in hashCode()
email is used by both equals() and hashCode(), but it can be changed after the object is inserted into the HashSet.
Issue #2: The object can become lost inside the HashSet
A HashSet uses the object's hash code to decide where to store it. If the hash code changes after insertion, the object may live in one bucket while future lookups search another bucket.
Issue #3: The bug is surprising and hard to debug
The exact same object reference was added to the set, but contains(user) can still return false. That is the kind of behavior that looks impossible until you know how hash-based collections work.
Issue #4: Identity and mutability are mixed together
If email defines the user's identity, changing it after the object is placed into hash-based collections is risky. The design should make identity stable or avoid using the mutable object as the set element.
Why This Happens
HashSet is backed by a hash table. When an object is inserted, Java uses hashCode() to choose a bucket. When you later call contains(), Java calculates the hash code again and searches the corresponding bucket.
hashCode() changes after insertion, the object may no longer be searchable in the bucket where Java now expects it to be.The problem is not that equals() and hashCode() exist. The problem is that they depend on mutable state.
Safer Version: Make the Key Fields Immutable
If email is part of the object's identity, make it immutable.
import java.util.Objects;
final class User {
private final String email;
User(String email) {
this.email = email;
}
String getEmail() {
return email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}Now the hash code cannot change after the object is placed into a HashSet.
Alternative: Use a Stable ID
In many applications, fields like email, display name, phone number, or address can change. A stable ID is usually a better identity field.
import java.util.Objects;
import java.util.UUID;
class User {
private final UUID id;
private String email;
User(UUID id, String email) {
this.id = id;
this.email = email;
}
void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}Here, email can change without changing the object's hash code, because equality is based on the stable id.
What A Strong Review Might Say
A strong code review does not stop at "this might return false." It explains the design risk clearly.
equals() and hashCode(). If an instance is added to a HashSet and then the email changes, lookups may fail. Consider making identity fields immutable or basing equality on a stable ID.Interview Follow-Ups
Is it always wrong for a HashSet element to be mutable?
Not always. It is risky when the mutable fields are used by equals() or hashCode(). Mutating unrelated fields is usually not a problem for the set's lookup behavior.
What if I remove the object, mutate it, and add it back?
That can work if done carefully, because the collection gets a chance to place the object into the correct bucket again. But this is easy to get wrong and can be a design smell.
Should equals() and hashCode() use every field?
No. They should use the fields that define logical identity. In many domain models, that means a stable ID, not every mutable business attribute.
How is this different from missing equals() and hashCode()?
Missing equals() and hashCode() means logically equal objects may not be treated as equal. This issue is different: equals() and hashCode() exist, but they depend on mutable fields that can change after insertion.
Final Takeaway
HashSet or used as HashMap keys should have stable equality and hash code behavior. If a field participates in equals() or hashCode(), be very careful about allowing that field to change.