I came across this interesting question during a technical interview.
public class Employee {
public int id;
public String name;
public int salary;
public Employee(String name, int id, int salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
@Override
public int hashCode() {
return Objects.hash(id, name, salary);
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
return id == other.id && Objects.equals(name, other.name) && salary == other.salary;
}
}
public class Main {
public static void main(String[] args) {
Employee e1 = new Employee("Sam", 1, 20000);
Employee e2 = new Employee("Sam", 1, 20000);
Employee e3 = new Employee("Sam", 2, 20000);
HashSet<Employee> hs = new HashSet<>();
hs.add(e1);
hs.add(e2); // same as e1 β won't be added
hs.add(e3); // different id β will be added
System.out.println("Before update--->");
for (Employee e : hs) {
System.out.println(e);
}
// Change a field that is part of hashCode/equals
e3.id = 1;
System.out.println("After update--->");
for (Employee e : hs) {
System.out.println(e);
}
}
}
Expected Output
Before update--->
Employee [id=2, name=Sam, salary=20000]
Employee [id=1, name=Sam, salary=20000]After update--->
Employee [id=1, name=Sam, salary=20000]
Employee [id=1, name=Sam, salary=20000](Order may vary as HashSet does not guarantee iteration order)
β Why Does This Happen?
We know HashSet doesnβt allow duplicates, so why does the second print still show two identical employees?
When you add an element, HashSet:
- Calls hashCode() to find the correct bucket.
- If thereβs already an element with the same hash, it calls equals() to check for equality.
- If they are equal, it does not add the element.
This check happens only at insertion.
When you change e3.id after itβs already in the set, it's hashCode() result changes.
However, the object remains in the bucket for its old hash value β HashSet does not automatically rehash or move elements when their data changes.
This causes the set to appear to contain duplicates and can also make contains() or remove() behave unexpectedly.
π― How to Prevent This
The safest way is to make objects immutable when they are used as keys in hash-based collections.
Example fix:
public class Employee {
private final int id;
private final String name;
private final int salary;
public Employee(String name, int id, int salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
@Override
public int hashCode() {
return Objects.hash(id, name, salary);
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
return id == other.id && Objects.equals(name, other.name) && salary == other.salary;
}
}
β Changes:
- Fields are private and final.
- No setters provided β prevents accidental modification.
π Takeaway
When using HashSet or any HashMap keys:
- Never mutate fields that participate in equals() and hashCode() after insertion.
- Prefer immutable classes for keys.
- If mutation is unavoidable:
- Remove the object from the set.
- Update it.
- Then re-add it.
Top comments (0)