« WebSocketRemoteのデモ映像を作った | メイン | iOSのSafariがWebSocketに対応したので、WebSocketRemote for iOSを作りました »

HashMap.keySetは毎回新しいインスタンスを作成しているわけではない

複数の人に勧められて、Effective Java 第2版を読んでいたら予想外のことが書いてあった。
たとえば、MapインターフェースのkeySetメソッドは、そのマップ内のすべてのキーから構成されているMapオブジェクトのSetビューを返します。一見、keySetの呼び出しは、その都度、新たなSetインスタンスを生成しなければならないように見えますが、あるMapオブジェクトに対するkeySetの呼び出しすべてが、同じSetインスタンスを返しても構いません。返されたSetインスタンスはたいていは可変ですが、返されたオブジェクトはすべて機能的に同じです。つまり、1つの返されたオブジェクトが変更されたら、他の返されたオブジェクトすべても変更されます。なぜならば、それらのオブジェクトはすべて、同じMapインスタンスによりバックアップされているからです。keySetビューオブジェクトの複数インスタンスを生成しても無害ですが、複数生成する必要もありません。
今まで、keySetメソッドで取得したSetオブジェクトは、下記のような動作をすると思っていた。
HashMap hash=new HashMap();
hash.put("a", "aaa");
Set key1 = hash.keySet();
System.out.println(key1);              //=>[a]
hash.put("b", "bbb");
System.out.println(key1);              //=>[a]
key1.add("z");
System.out.println(key1);              //=>[a, z]
key1.remove("a");
System.out.println(key1);              //=>[z]
Set key2 = hash.keySet();
System.out.println(key2);              //=>[a, b]
System.out.println(key1==key2);        //=>false
System.out.println(hash);              //=>{a=aaa, b=bbb}
つまり、keySetメソッドで取得したSetオブジェクトは、それぞれ別のインスタンスであると考えていた。
しかし、実際は次のような動作をした。Javaのバージョンは1.6。
HashMap hash=new HashMap();
hash.put("a", "aaa");
Set key1 = hash.keySet();
System.out.println(key1);              //=>[a]
hash.put("b", "bbb");
System.out.println(key1);              //=>[b, a]
//key1.add("z");                       //=>error!
System.out.println(key1);              //=>[b, a]
key1.remove("a");
System.out.println(key1);              //=>[b]
Set key2 = hash.keySet();
System.out.println(key2);              //=>[b]
System.out.println(key1==key2);        //=>true
System.out.println(hash);              //=>{b=bbb}
HashMapオブジェクトhashのキーを変更すると、先に取得してたSetオブジェクトkey1の要素も変更されている。
別々のタイミングで取得した別々のSetオブジェクト1ey1とkey2が等値となっており、key2取得前にkey1を変更していても等値なのは変わらない。
中でも驚いたのが、Setオブジェクトkey1から要素を削除すると、HashMapの要素も削除されること。
HashMap.keySetをJavaDocを確認すると次のように書かれていた。
このマップに含まれるキーの Set ビューを返します。セットはマップと連動しているので、マップに対する変更はセットに反映され、また、セットに対する変更はマップに反映されます。セットの反復処理中にマップが変更された場合、反復処理の結果は定義されません (反復子自身の remove オペレーションを除く)。セットは要素の削除をサポートしており、対応するマッピングをマップから削除できます。削除は、Iterator.remove、Set.remove、removeAll、retainAll、および retainAll オペレーションを通して行います。Set は、add オペレーションや addAll オペレーションはサポートしていません。 
つまり、上記のような動作を望まないのであれば、keySetメソッドの戻り値を元に、新たにインスタンスを作成すればいい。
HashMap hash=new HashMap();
hash.put("a", "aaa");
Set key1 = new HashSet(hash.keySet());
System.out.println(key1);              //=>[a]
hash.put("b", "bbb");
System.out.println(key1);              //=>[a]
key1.add("z");
System.out.println(key1);              //=>[a, z]
key1.remove("a");
System.out.println(key1);              //=>[z]
Set key2 = new HashSet(hash.keySet());
System.out.println(key2);              //=>[a, b]
System.out.println(key1==key2);        //=>false
System.out.println(hash);              //=>{a=aaa, b=bbb}

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

Google

タグ クラウド