Java HashMap содержит ключ

У меня есть следующий код

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));
  }
}

Я ожидаю, что вывод будет ложным, истинным, истинным, ложным и истинным. Однако вывод будет ложным, истинным, ложным, ложным, ложным.

Я ищу, как вывод является ложным для 3-го и 5-го случая. Каково поведение HashMap containsKey?

Почему вывод ложный, хотя объект Key есть на карте. Оба метода equals и hashcode переопределены для класса Person.


person Tech Enthusiast    schedule 13.07.2020    source источник
comment
Пришлось добавить много избыточных деталей, так как SO не позволял мне из-за того, что контент был меньше кода. Я не вижу здесь ничего лишнего, кроме этого, и если бы вы представили код без текста, вопрос был бы значительно сложнее понять. Хотя ваш код мог бы быть короче, просто выполнив одну проверку (или, может быть, одну с ожидаемым результатом и одну с неожиданным), а не пять.   -  person Jon Skeet    schedule 13.07.2020
comment
По этой причине классы, используемые в качестве ключей в HashMaps, должны быть неизменяемыми.   -  person tgdavies    schedule 13.07.2020
comment
Связано: Неизменяемые объекты и ключи HashMap   -  person Lino    schedule 13.07.2020


Ответы (3)


Следующее утверждение нарушает вашу карту:

p.setName("charlie");

Это приводит к тому, что ключ, на который ссылается переменная p, больше не находится в ячейке, соответствующей его hashCode(), поскольку вы меняете его hashCode().

Вы никогда не должны изменять состояние ключа, который уже находится на карте, если это изменение влияет на результат hashCode() или equals().

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

Возвращает false, так как экземпляр Person с именем charlie не сопоставлен с той же ячейкой, что и экземпляр Person с именем alice. Поэтому containsKey() ищет p в корзине, совпадающей с именем Чарли, и не находит его там.

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

Возвращает false, так как p2 не равно p (у них разные имена).

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

Возвращает false, так как ключ p находится в корзине, которая соответствует имени alice, несмотря на то, что ее текущее имя — charlie, поэтому containsKey() ищет ее не в той корзине и не находит.

person Eran    schedule 13.07.2020
comment
Ах. Спасибо. Я упустил тот факт, что хэш-код теперь будет разрешаться в другое ведро, поскольку значение ключа изменилось. - person Tech Enthusiast; 13.07.2020
comment
@TechEnthusiast: На самом деле не имеет значения, разрешается ли он в другое ведро — я бы, конечно, ожидал, что карта проверит наличие одинаковых хэш-кодов (сохраненных и искомых ключей) перед вызовом equals . Даже если есть только одно ведро, если хэш-коды разные, его не найдут. - person Jon Skeet; 13.07.2020

Вы изменяете объект после добавления его в качестве ключа в HashMap таким образом, чтобы изменить хеш-код. Это все равно, что дать кому-то свои контактные данные, переехать, а потом все еще ожидать, что они смогут вас найти.

Когда вы добавляете ключ на карту, он сохраняет хэш-код. Когда вы пытаетесь найти ключ, карта запрашивает хэш-код ключа, который вы пытаетесь найти, и эффективно находит все записи с таким же сохраненным хеш-кодом. Поскольку новый хеш-код не соответствует старому хэш-коду, он не может найти кандидатов для проверки с помощью equals.

По сути, вы не должны изменять ничего, что влияет на хэш-код или равенство после использования объекта в качестве ключа на карте.

person Jon Skeet    schedule 13.07.2020

Чтобы разместить больше информации об ответе Эрана. Я проверил некоторые источники 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;
}

В третьем случае хеш-значение ключа в узле остается прежним, даже если вы изменили его имя на Чарли. Вот почему он возвращает ложь. Кажется, что вы НИКОГДА не должны менять ключ объекта, учитывая тот факт, что это нарушит карту из-за несоответствия хэша (ключа)

person efexemus    schedule 13.07.2020