6

I have the following code

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Person {
  private String name;
  private long birthTime;

  @Override
  public int hashCode() {
    return Objects.hash(name, birthTime);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof Person)) {
      return false;
    }
    Person other = (Person) obj;
    return Objects.equals(name, other.name)
        && birthTime == other.birthTime;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public long getBirthTime() {
    return birthTime;
  }

  public void setBirthTime(long birthTime) {
    this.birthTime = birthTime;
  }

  public static Person person(String name, long time) {
    Person p = new Person();
    p.setName(name);
    p.setBirthTime(time);
    return p;
  }

  public static void main(String[] args) {
    Map<Person, Person> map = new HashMap<>();
    Person p = person("alice", 3);
    System.out.println("1. " + map.containsKey(p));

    map.put(p, p);
    System.out.println("2. " + map.containsKey(p));

    p.setName("charlie");
    System.out.println("3. " + map.containsKey(p));

    Person p2 = person("alice", 3);
    System.out.println("4. " + map.containsKey(p2));

    Person p3 = person("charlie", 3);
    System.out.println("5. " + map.containsKey(p3));
  }
}

I am expecting the output to be false, true, true, false and true. However, the output is false, true, false, false, false.

I am looking for how the output is false for the 3rd and 5th case. What is the behavior of HashMap containsKey?

Why is the output false even though the Key object is there in the Map. The equals and hashcode methods are both overridden for the Person class.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • 1
    "Had to add a lot of redundant details as SO was not allowing me due to content being smaller than code." I don't see anything redundant here other than that, and if you'd presented the code without the text, the question would have been significantly harder to understand. Although your code could have been shorter by basically just doing *one* check (or maybe one with the expected result and one with an unexpected one) rather than five. – Jon Skeet Jul 13 '20 at 07:01
  • 1
    Classes used as keys in HashMaps should be immutable for this reason – tgdavies Jul 13 '20 at 07:02
  • Related: [Immutable objects and HashMap keys](https://stackoverflow.com/questions/20212440/immutable-objects-and-hashmap-keys) – Lino Jul 13 '20 at 07:32

3 Answers3

5

The following statement breaks your Map:

p.setName("charlie");

It causes the key referenced by the variable p to no longer be positioned in the bin that matches its hashCode(), since you are changing its hashCode().

You should never change the state of a key that is already in the Map if that change affects the result of hashCode() or equals().

p.setName("charlie");
System.out.println("3. " + map.containsKey(p));

Returns false since a Person instance whose name is "charlie" is not mapped to the same bin as a Person instance whose name is "alice". Therefore containsKey() searches for p in the bin matching the name "charlie", and doesn't find it there.

Person p2 = person("alice", 3);
System.out.println("4. " + map.containsKey(p2));

Returns false since p2 is not equal to p (they have different names).

Person p3 = person("charlie", 3);
System.out.println("5. " + map.containsKey(p3));

Returns false since the key p is located in the bin that matches the name "alice", even though its current name is "charlie", so containsKey() searches for it in the wrong bin, and doesn't find it.

Eran
  • 387,369
  • 54
  • 702
  • 768
  • Ah. Thank you. I missed the fact that the hashCode would now resolve to a different bucket since the Key value is changed. – Tech Enthusiast Jul 13 '20 at 07:14
  • 1
    @TechEnthusiast: It doesn't really matter whether it resolves to a different bucket - I'd certainly *expect* the map to check for equal hash codes (stored and "key we're looking for") before calling `equals`. Even if there's just one bucket, if the hash codes are different, it won't be found. – Jon Skeet Jul 13 '20 at 07:28
3

You're modifying the object after adding it as a key in the HashMap, in a way that changes the hash code. That's like giving someone your contact details, moving house, and then still expecting them to be able to find you.

When you add a key to the map, it stores the hash code. When you try to find a key, the map asks for the hash code of the key you're trying to find, and efficiently finds any entries with the same stored hash code. As the "new" hash code doesn't match the "old" hash code, it can't find any candidates to check with equals.

Basically, you shouldn't modify anything that affects the hash code or equality after using the object as a key in the map.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
0

To put more information about Eran answer. I have checked some the source of HashMap.

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    ...
        tab[i] = newNode(hash, key, value, null);
    ...
}

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

In you 3rd case, the hash value of the key in "node" stay the same even you changed its' name to "Charlie". That's why it return false. It seems that you should NEVER change the object key given the fact that it would break the map for the mismatch of hash(key)

efexemus
  • 3
  • 3