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の設計バグ?」という気もするが、そこまで言うのも可哀そうな気がする。まあ、使う側としては、気をつける必要ありますね。