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;
}
}
Ответы
Ответ 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;
}
}