読者です 読者をやめる 読者になる 読者になる

snuffkinの遊び場

IT関係、スポーツ、数学等に関することを、気が向いたときに書いてます。

HashSetの中身を直接書き換えると、containsが効かなくなる

今日、他人の問題を調査したときに、JDKのソースを読んで気づいたのですが、HashSetの中身を直接書き換えると、containsが効かなくなるんですね。

以下にサンプルを示します。お手軽サンプルなので、不作法な点は見逃してください。
まずは、Setに入れるクラス。hashCodeやequalsを実装しておきます。また、何かしらの属性を持っているとします。

package sample;

public class Hoge {
	private int id;
	public Hoge(int id) {
		this.id = id;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	@Override
	public int hashCode() {
		return id;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Hoge) {
			return (this.id == ((Hoge) obj).id);
		}
		return false;
	}
}

で、実行コードは以下の通り。

package sample;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class TestHash {
	public static void main(String[] args) {
		Hoge hoge1 = new Hoge(1);
		Hoge hoge2 = new Hoge(2);
		
		// HashSetにデータを入れる
		Set<Hoge> hogeList = new HashSet<Hoge>();
		hogeList.add(hoge1);
		
		// ここの実行結果は直感通り
		System.out.println("hoge1 hash? " + hoge1.hashCode());
		System.out.println("hoge1 contains? " + hogeList.contains(hoge1));
		System.out.println("hoge2 hash? " + hoge2.hashCode());
		System.out.println("hoge2 contains? " + hogeList.contains(hoge2));
		
		// HashSetの中身を直接修正
		Iterator<Hoge> ite = hogeList.iterator();
		while (ite.hasNext()) {
			Hoge temp = ite.next();
			temp.setId(2);
		}
		
		// あれ? と思う実行結果
		System.out.println("hoge1 hash? " + hoge1.hashCode());
		System.out.println("hoge1 contains? " + hogeList.contains(hoge1));
	}
}

何をやってるかと言うと、以下の通り。

  • HashSetにデータを入れる
  • containsで中身をチェック
  • HashSet内部のデータを修正
  • containsで中身をチェック

で、実行結果は次のようになります。

hoge1 hash? 1
hoge1 contains? true
hoge2 hash? 2
hoge2 contains? false
hoge1 hash? 2
hoge1 contains? false

問題は、2回目の「hogeList.contains(hoge1))」の結果がfalseになること。
直感的には「whileの中で書き換えたけど、HashSetの中身もhoge1も両方書き換えたので、trueになるのでは?」と思います。ですが、falseになります。これまた、びっくりするのは、falseになったあと、再度iteratorで取って中身をequalsで比較すると、ちゃんとtrueになります。
実は「equalsでtrueとなるデータが中身にあっても、containsでfalseになることがある」のです。それが、この場合。

java.util.HashMapにputした要素は内部的に配列となっているのですが、この内部配列のインデックスはput時のhashCodeで決定されます。このとき、iteratorにして要素の中身を変更すると、hashCodeで計算される値は変更されるのに、配列のインデックスは変化しません。したがって、変更後の要素をcontainsで探そうとすると、hashCodeの値がずれるため、equalsでtrueとなる要素を持っているにも関わらず、containsの結果はfalseとなります。

う〜ん、知らなかった。「HashMapの設計バグ?」という気もするが、そこまで言うのも可哀そうな気がする。まあ、使う側としては、気をつける必要ありますね。