オブジェクトを保存するための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;
}
}
キーをマップに挿入した後、キーを変更しないでください。
編集: 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のみが取得されます
Arnaud Denoyelleが指摘しているように、キーを変更するとこの効果が得られます。 reasonは、containsKey
がハッシュマップ内のキーのバケットを処理するのに対し、反復子は処理しないことです。マップの最初のキー-バケットを無視する-がたまたま必要なものである場合、表示されている動作を取得できます。マップにエントリが1つしかない場合、これはもちろん保証されます。
シンプルな2バケットマップを想像してください。
_[0: empty] [1: yourKeyValue]
_
イテレータは次のようになります。
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
_フィールドは変更されません)、マップが壊れることも意味します。
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;
}
}