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


}

26
2018-02-06 10:33


起源


你能提供吗? SSCCE  sscce.org ? - RobAu
密钥已被修改或您的实施 equals()/hashcode 是错的。 - Arnaud Denoyelle
你应该避免 if statements 没有花括号。 - Arnaud Denoyelle
哈希码只是查找密钥的第一步。如果您的新对象不等于旧对象,则找不到该键。 - Arnaud Denoyelle
你有没有调试过 containsKey 用Eclipse的方法?你可以看到它失败的地方。 - Sentry


答案:


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

编辑:我找到了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()那么你只会得到真实


19
2018-02-06 11:01



这就是我所期望的。但我不改变hashCode中使用的字段(等于) - agad


正如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 字段 - 将打破地图。


10
2018-02-06 11:06





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

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

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


3
2018-06-03 22:15





这是 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;
    }


}

1
2018-02-06 11:07



似乎,钥匙真的发生了什么。但是如果equals和hashCode返回true会出现什么问题? (以下代码有效:for(Entry <T,U> b:fields.entrySet()){if(b.getKey()。equals(bean)&& b.getValue()。isChecked()){return true;} } return false;) - agad
问题在于hashCode和equals方法 country 领域。当我修复它们时,一切都已开始正常工作:$ - agad
@agad怎么样和为什么? - Mingliang Liu