Java HashMap containsKey为现有对象返回false

我有一个用于存储对象的HashMap:

private Map fields = Collections.synchronizedMap(new HashMap()); 

但是,当试图检查密钥的存在时, containsKey方法返回false
equalshashCode方法,但找不到密钥。
调试一段代码时:

  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 implements Serializable{   @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; } } 

将密钥插入地图后,您不应修改密钥。

编辑:我在Map中找到了javadoc的摘录:

注意:如果将可变对象用作映射键,则必须非常小心。 如果在对象是地图中的键的同时以影响等于比较的方式更改对象的值,则不指定映射的行为。

使用简单包装类的示例:

 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 map = new HashMap(); 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 

注意:如果你不重写hashcode()那么你只会得到真实

正如Arnaud Denoyelle所指出的那样,修改密钥会产生这种影响。 原因containsKey关心哈希映射中的密钥桶,而迭代器则不关心。 如果地图中的第一个键 – 忽略存储桶 – 恰好是您想要的那个,那么您可以获得您所看到的行为。 如果地图中只有一个条目,这当然是有保证的。

想象一个简单的双桶地图:

 [0: empty] [1: yourKeyValue] 

迭代器是这样的:

  • 迭代桶0中的所有元素:没有
  • 迭代桶1中的所有元素:只是一个yourKeyValue

但是, containsKey方法如下所示:

  • keyToFind有一个hashCode() == 0 ,所以让我看看桶0( 只有那里)。 哦,它是空的 – 返回false.

事实上,即使密钥停留在同一个桶中,您仍然会遇到此问题! 如果查看HashMap的实现,您将看到每个键值对与键的哈希码一起存储。 当地图想要针对传入的密钥检查存储的密钥时,它使用此hashCode和密钥的equals

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

这是一个很好的优化,因为它意味着碰巧碰撞到同一个桶中的具有不同hashCodes的密钥将被视为非等价非常便宜(只是一个int比较)。 但这也意味着更改密钥 – 不会更改存储的e.key字段 – 将破坏地图。

调试java源代码我意识到方法containsKey检查搜索键上的两个东西对着密钥集中的每个元素: hashCodeequals ; 它按顺序执行。

这意味着如果obj1.hashCode() != obj2.hashCode() ,它返回false(不评估obj1.equals(obj2)。但是,如果obj1.hashCode() == obj2.hashCode() ,则返回obj1.equals(obj2)

您必须确保两种方法 – 可能必须覆盖它们 – 对于您定义的标准,评估为true。

以下是SSCCE的问题。 它就像一个魅力,它不可能是其他的,因为你的hashCodeequals方法似乎是由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 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 { private Map fields = Collections.synchronizedMap(new HashMap()); 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; } }