Java HashMap 包含Key 为现有对象返回 false

2022-09-01 05:19:48

我有一个用于存储对象的哈希图:

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

但是,当尝试检查密钥是否存在时,方法返回 。
和方法已实现,但找不到键。
调试一段代码时:containsKeyfalseequalshashCode

    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

在将密钥插入地图后,您不得对其进行修改。

编辑:我在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<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

注意:如果你不覆盖hashcode(),那么你只会得到true


答案 2

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

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

[0: empty]  [1: yourKeyValue]

迭代器如下所示:

  • 迭代存储桶 0 中的所有元素:没有
  • 迭代存储桶 1 中的所有元素:仅一个yourKeyValue

但是,该方法是这样的:containsKey

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

事实上,即使密钥保持在同一桶中,您仍然会遇到此问题!如果您查看 的实现,您将看到每个键值对都与键的哈希代码一起存储。当映射想要根据传入的密钥检查存储的密钥时,它会同时使用此哈希代码和密钥的等于HashMap

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

这是一个很好的优化,因为这意味着具有不同哈希码的键碰巧碰撞到同一存储桶中将被视为不相等,非常便宜(只是一个比较)。但这也意味着更改键 ( 不会更改存储的字段 ) 将破坏地图。inte.key