Reputation: 169
My Custom class that will be contained by HashSet
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"hashcode='" + this.hashCode() + '\'' +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (age != person.age) return false;
if (!name.equals(person.name)) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + age;
return result;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
My HashSet test that fails
public void hashSetTest() {
Set<Person> personSet = new HashSet<Person>();
Person p1 = new Person("raghu", 12);
Person p2 = new Person("rimmu", 21);
personSet.add(p1);
personSet.add(p2);
p1.setName("raghus");
p1.setAge(13);
int i2 =p1.hashCode();
System.out.println(personSet.size() + ": "+ p1.hashCode()+" : "+personSet.contains(p1)+ " : "+i2);
}
Iam expecting personSet.contains(p1) to pass. Why is it returning false? Thanks sri
Upvotes: 16
Views: 25853
Reputation: 11
Hashes are simple pairing of key and values. Here's how the state of your code would look before and after the renaming in pseudo-code:
Before:
personSet => {
SOME_NUM1 => Person(name=>"raghu", 12),
SOME_NUM2 => Person(name=>"rimmu", 21)
}
p1.setName("raghus"); #p1.hashcode() = SOME_NEW_NUM
p1.setAge(13);#p1.hashcode() = SOME_OTHER_NEW_NUM
After:
personSet => {
SOME_NUM1 => Person(name=>"raghu", 13),
SOME_NUM2 => Person(name=>"rimmu", 21)
}
Since you have direct access to the p1 the object within the HashSet is updated correctly, but HashSet does not pay attention to contained objects hashcodes being updated. When a call to personSet.contains(p1)
is called, the HashSet looks for an entry with the new value of p1.hashcode()
.
The p1
object is associated with its previous hashcode at the time when it was added to the HashSet.
Upvotes: 1
Reputation: 691
I think you need to have hashCode depends on mutable fields quite often indeed: when you override equals that depends on mutable fields.
From hashCode's contract: "If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result."
So, if you create two objects such that A.equals(B) is true, and then modify A such a way that you get A.equals(B) became false, you need to have hashCodes change too.
It's true that in hashCode's documentation is stated that "It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results.", but I don't know how this can help.
Upvotes: 1
Reputation: 5499
HashSet implements Set. The ApiDoc specifies:
Note: Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
In your example this is the case, because changing name
or age
on p1
affects equal-comparison. So according the ApiDoc, the behavior of Set in your case is unspecified.
Upvotes: 5
Reputation: 363467
Because p1.hashCode()
changes when you modify p1
, so it can't be found at its original index in the hash table anymore. Never let a hash value depend on a mutable field.
(You're quite lucky that it fails during testing; it might just as well have succeeded, only to fail in production.)
Upvotes: 31