web-dev-qa-db-ja.com

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


}
30
agad

キーをマップに挿入した後、キーを変更しないでください。

編集: 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のみが取得されます

24

Arnaud Denoyelleが指摘しているように、キーを変更するとこの効果が得られます。 reasonは、containsKeyがハッシュマップ内のキーのバケットを処理するのに対し、反復子は処理しないことです。マップの最初のキー-バケットを無視する-がたまたま必要なものである場合、表示されている動作を取得できます。マップにエントリが1つしかない場合、これはもちろん保証されます。

シンプルな2バケットマップを想像してください。

_[0: empty]  [1: yourKeyValue]
_

イテレータは次のようになります。

  • バケット0のすべての要素を反復処理します:なし
  • バケット1のすべての要素を反復処理します:ただ1つのyourKeyValue

ただし、containsKeyメソッドは次のようになります。

  • keyToFindにはhashCode() == 0があるので、バケット0(およびonlyがあります)を見てみましょう。ああ、空です-_false._を返します

実際、キーが同じバケットに残っていても、stillこの問題が発生します! HashMapの実装を見ると、各キーと値のペアが保存されていることがわかりますキーのハッシュコードと共に。マップが保存されたキーを着信キーと照合する場合、 このhashCodeとキーのequals の両方を使用します。

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

これはニースの最適化です。これは、同じバケットに偶然に衝突する異なるhashCodesを持つキーが、非常に安価に等しくない(単にint比較)と見なされることを意味するためです。ただし、キーを変更すると(保存されている_e.key_フィールドは変更されません)、マップが壊れることも意味します。

10
yshavit

JavaソースコードのデバッグメソッドcontainsKeyは、キーセット内のすべての要素に対して、検索されたキーの2つのことをチェックすることに気付きました:hashCodeおよびequals;そして、それはその順番で行います。

つまり、obj1.hashCode() != obj2.hashCode()の場合、falseを返します(obj1.equals(obj2)を評価せずに。ただし、obj1.hashCode() == obj2.hashCode()の場合、obj1.equals(obj2)を返します。

両方のメソッド(オーバーライドする必要がある場合があります)が、定義された基準に対してtrueに評価されていることを確認する必要があります。

以下に、問題の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
Andremoniy