Подтвердить что ты не робот

Java HashMap containsKey возвращает false для существующего объекта

У меня есть HashMap для хранения объектов:

    private Map<T, U> fields = Collections.synchronizedMap(new HashMap<T, U>());

но при попытке проверить существование ключа метод containsKey возвращает false.
equals и hashCode реализованы, но ключ не найден.
При отладке фрагмента кода:

    return fields.containsKey(bean) && fields.get(bean).isChecked();

У меня есть:

   bean.hashCode() = 1979946475 
   fields.keySet().iterator().next().hashCode() = 1979946475    
   bean.equals(fields.keySet().iterator().next())= true 
   fields.keySet().iterator().next().equals(bean) = true

но

fields.containsKey(bean) = false

Что может вызвать такое странное поведение?

public class Address extends DtoImpl<Long, Long> implements Serializable{

   <fields>
   <getters and setters>

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + StringUtils.trimToEmpty(street).hashCode();
    result = prime * result + StringUtils.trimToEmpty(town).hashCode();
    result = prime * result + StringUtils.trimToEmpty(code).hashCode();
    result = prime * result + ((country == null) ? 0 : country.hashCode());
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Address other = (Address) obj;
    if (!StringUtils.trimToEmpty(street).equals(StringUtils.trimToEmpty(other.getStreet())))
        return false;
    if (!StringUtils.trimToEmpty(town).equals(StringUtils.trimToEmpty(other.getTown())))
        return false;
    if (!StringUtils.trimToEmpty(code).equals(StringUtils.trimToEmpty(other.getCode())))
        return false;
    if (country == null) {
        if (other.country != null)
            return false;
    } else if (!country.equals(other.country))
        return false;
    return true;
}


}
4b9b3361

Ответ 1

Вы не должны изменять ключ после его вставки на карту.

Изменить: я нашел экстракт javadoc в Map:

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

Пример с простым классом-оболочкой:

public static class MyWrapper {

  private int i;

  public MyWrapper(int i) {
    this.i = i;
  }

  public void setI(int i) {
    this.i = i;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return i == ((MyWrapper) o).i;
  }

  @Override
  public int hashCode() {
    return i;
  }
}

и тест:

public static void main(String[] args) throws Exception {
  Map<MyWrapper, String> map = new HashMap<MyWrapper, String>();
  MyWrapper wrapper = new MyWrapper(1);
  map.put(wrapper, "hello");
  System.out.println(map.containsKey(wrapper));
  wrapper.setI(2);
  System.out.println(map.containsKey(wrapper));
}

Выход:

true
false

Ответ 2

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

Представьте себе простое двухкартовое отображение:

[0: empty]  [1: yourKeyValue]

Итератор выглядит следующим образом:

  • итерация по всем элементам в ведре 0: нет
  • итерация по всем элементам в ведре 1: только один yourKeyValue

Метод containsKey, однако, выглядит следующим образом:

  • keyToFind имеет hashCode() == 0, поэтому позвольте мне посмотреть в ведро 0 (и только там). О, он пуст - верните false.

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

((k = e.key) == key || (key != null && key.equals(k))))

Это хорошая оптимизация, так как это означает, что ключи с разными хэш-кодами, которые могут столкнуться в одном и том же ведре, будут считаться не равными очень дешево (просто сравнение int). Но это также означает, что смена ключа - который не изменит сохраненное поле e.key - сломает карту.

Ответ 3

Отладка исходного кода java Я понял, что метод containsKey проверяет две вещи на найденном ключе против каждого элемента в наборе ключей: hashCode и равно; и он делает это в этом порядке.

Это означает, что если obj1.hashCode() != obj2.hashCode(), он возвращает false (без оценки obj1.equals(obj2). Но если obj1.hashCode() == obj2.hashCode(), то он возвращает obj1.equals(obj2)

Вы должны быть уверены, что оба метода - возможно, вам придется переопределить их - оцените значение true для ваших определенных критериев.

Ответ 4

Ниже приведена SSCCE для вашей проблемы. Он работает как шарм, и это не может быть иначе, потому что ваши методы hashCode и equals кажутся автогенерируемыми IDE, и они выглядят отлично.

Итак, ключевым словом является when debugging. Сама отладка может нанести вред вашим данным. Например, где-то в окне отладки вы устанавливаете выражение, которое изменяет объект fields или bean. После этого ваши другие выражения дадут вам неожиданный результат.

Попробуйте добавить все эти проверки внутри вашего метода, откуда вы получили оператор return и распечатать их результаты.

import org.apache.commons.lang.StringUtils;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class Q21600344 {

    public static void main(String[] args) {
        MapClass<Address, Checkable> mapClass = new MapClass<>();
        mapClass.put(new Address("a", "b", "c", "d"), new Checkable() {
            @Override
            public boolean isChecked() {
                return true;
            }
        });

        System.out.println(mapClass.isChecked(new Address("a", "b", "c", "d")));
    }

}

interface Checkable {
    boolean isChecked();
}

class MapClass<T, U extends Checkable> {
    private Map<T, U> fields = Collections.synchronizedMap(new HashMap<T, U>());

    public boolean isChecked(T bean) {
        return fields.containsKey(bean) && fields.get(bean).isChecked();
    }

    public void put(T t, U u) {
        fields.put(t, u);
    }
}

class Address implements Serializable {

    private String street;
    private String town;
    private String code;
    private String country;

    Address(String street, String town, String code, String country) {
        this.street = street;
        this.town = town;
        this.code = code;
        this.country = country;
    }

    String getStreet() {
        return street;
    }

    String getTown() {
        return town;
    }

    String getCode() {
        return code;
    }

    String getCountry() {
        return country;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + StringUtils.trimToEmpty(street).hashCode();
        result = prime * result + StringUtils.trimToEmpty(town).hashCode();
        result = prime * result + StringUtils.trimToEmpty(code).hashCode();
        result = prime * result + ((country == null) ? 0 : country.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Address other = (Address) obj;
        if (!StringUtils.trimToEmpty(street).equals(StringUtils.trimToEmpty(other.getStreet())))
            return false;
        if (!StringUtils.trimToEmpty(town).equals(StringUtils.trimToEmpty(other.getTown())))
            return false;
        if (!StringUtils.trimToEmpty(code).equals(StringUtils.trimToEmpty(other.getCode())))
            return false;
        if (country == null) {
            if (other.country != null)
                return false;
        } else if (!country.equals(other.country))
            return false;
        return true;
    }


}