JAVA

【JAVA】各バージョンによるHashMapのIndex割り当ての違い

2017年1月22日

JavaのバージョンによりHashMapのIndexを求める方法が異なっていたのでメモ

各バージョンによる処理の違い

各バージョンのHashMapはどのように処理をしているのかソースにて見比べることにしました。

put、hashメソッド部分のみを抽出。

J2SE 1.4

インデックス番号の割り出し方法は
キー値からハッシュ値出力⇒ハッシュ値をビット演算⇒テーブルサイズでマスク
の順に行っています。


public Object put(Object key, Object value) {
    Object k = maskNull(key);
    int hash = hash(k);
    int i = indexFor(hash, table.length);
    for (Entry e = table[i]; e != null; e = e.next) {
        if (e.hash == hash && eq(k, e.key)) {
            Object oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, k, value, i);
    return null;
}

static int hash(Object x) {
    int h = x.hashCode();
    h += ~(h << 9);
    h ^=  (h >>> 14);
    h +=  (h << 4);
    h ^=  (h >>> 10);
    return h;
}

static int indexFor(int h, int length) {
    return h & (length-1);
}

J2SE 1.5


public V put(K key, V value) {
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
        modCount++;
        addEntry(hash, key, value, i);
        return null;
}
    
static int hash(int h) {
    return useNewHash ? newHash(h) : oldHash(h);
}
    
private static int newHash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
    
private static int oldHash(int h) {
    h += ~(h << 9);
    h ^=  (h >>> 14);
    h +=  (h << 4);
    h ^=  (h >>> 10);
    return h;
}
    
static int indexFor(int h, int length) {
    return h & (length-1);
}
    
/**
 * Whether to prefer the old supplemental hash function, for
 * compatibility with broken applications that rely on the
 * internal hashing order.
 *
 * Set to true only by hotspot when invoked via
 * -XX:+UseNewHashFunction or -XX:+AggressiveOpts
 */
private static final boolean useNewHash;
static { useNewHash = false; }
    

Eclipseの場合、VM引数オプションに追加することでnewHashメソッドで処理できるようになります。

Java SE 1.6

J2SE 1.5のnewHashのみになったみたいです。


public V put(K key, V value) {
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
        return oldValue;
        }
    }
    
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
    
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
    
static int indexFor(int h, int length) {
    return h & (length-1);
}

Java SE 1.7

hashSeedというハッシュの初期化値が0以外かつキー値がString型の場合は別処理にてハッシュ値を求めるようです。(デフォルト:0)
上記以外はJava SE 1.6と同じかと思いきやキー値のハッシュ値と0でXOR演算しているため若干処理が変わってるようです。

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
    
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
    
static int indexFor(int h, int length) {
    return h & (length-1);
}

Java SE 1.8

処理の見直しがされたようで今までのとはかなり変わっています。

内容的にはハッシュコードが衝突した場合のパフォーマンス改善を行っているようです。

使ってあれこれキー値の検索をしているようです。


public V put(K arg0, V arg1) {
    return this.putVal(hash(arg0), arg0, arg1, false, true);
}
    
final V putVal(int arg0, K arg1, V arg2, boolean arg3, boolean arg4) {
    HashMap.Node[] arg5 = this.table;
    int arg7;
    if(this.table == null || (arg7 = arg5.length) == 0) {
        arg7 = (arg5 = this.resize()).length;
    }
    
    Object arg6;
    int arg8;
    if((arg6 = arg5[arg8 = arg7 - 1 & arg0]) == null) {
       arg5[arg8] = this.newNode(arg0, arg1, arg2, (HashMap.Node)null);
    } else {
        Object arg9;
        label79: {
            Object arg10;
            if(((HashMap.Node)arg6).hash == arg0) {
                arg10 = ((HashMap.Node)arg6).key;
                if(((HashMap.Node)arg6).key == arg1 || arg1 != null && arg1.equals(arg10)) {
                    arg9 = arg6;
                    break label79;
                }
            }

            if(arg6 instanceof HashMap.TreeNode) {
                arg9 = ((HashMap.TreeNode)arg6).putTreeVal(this, arg5, arg0, arg1, arg2);
            } else {
            int arg11 = 0;

            while(true) {
                arg9 = ((HashMap.Node)arg6).next;
                if(((HashMap.Node)arg6).next == null) {
                    ((HashMap.Node)arg6).next = this.newNode(arg0, arg1, arg2, (HashMap.Node)null);
                    if(arg11 >= 7) {
                        this.treeifyBin(arg5, arg0);
                    }
               break;
                }

                if(((HashMap.Node)arg9).hash == arg0) {
                    arg10 = ((HashMap.Node)arg9).key;
                    if(((HashMap.Node)arg9).key == arg1 || arg1 != null && arg1.equals(arg10)) {
                        break;
                    }
                }

                arg6 = arg9;
                ++arg11;
            }
        }
    }

    if(arg9 != null) {
        Object arg12 = ((HashMap.Node)arg9).value;
        if(!arg3 || arg12 == null) {
            ((HashMap.Node)arg9).value = arg2;
        }

        this.afterNodeAccess((HashMap.Node)arg9);
        return arg12;
        }
    }

    ++this.modCount;
    if(++this.size > this.threshold) {
        this.resize();
    }

    this.afterNodeInsertion(arg4);
    return null;


}

テスト

import java.util.HashMap;
import java.util.Iterator;

public class HashMapTest {
    public static void main(String args[]){
        HashMap testMap = new HashMap();
        testMap.put("aaa", "111");
        testMap.put("bbb", "222");
        testMap.put("ccc", "333");
        testMap.put("ddd", "444");
        testMap.put("eee", "555");
        testMap.put("fff", "666");
        testMap.put("ggg", "777");
        testMap.put("hhh", "888");

        for(Iterator iterator = testMap.keySet().iterator(); iterator.hasNext();){
            String key = (String) iterator.next();
            String value = (String) testMap.get(key);
            System.out.println(key+":"+value);
        }
    }
}

1.4~1.8のバージョンを上記ソースで行っています。(1.4に合わせたソースになってます。)
実行結果(コンソール出力)とHashMap内の配列状況をまとめました。

結果

配列はEclipseのデバックモードにてtestMap.tableで取得した値です。
()内は同じIndex番号に入っている値です。

J2SE 1.5はnewは起動オプション(-XX:+UseNewHashFunction)を設定しています。

 実行結果配列
1.4fff:666
ccc:333
aaa:111
ggg:777
eee:555
ddd:444
bbb:222
hhh:888
[hhh=888, null, null, null, null, null, bbb=222, ddd=444, ggg=777(eee=555),
null, null, aaa=111, ccc=333, null, fff=666, null]
1.5
(old)
fff:666
ccc:333
aaa:111
ggg:777
eee:555
ddd:444
bbb:222
hhh:888
[hhh=888, null, null, null, null, null, bbb=222, ddd=444, ggg=777(eee=555),
null, null, aaa=111, ccc=333, null, fff=666, null]
1.5
(new)
eee:555
bbb:222
ccc:333
ggg:777
ddd:444
aaa:111
fff:666
hhh:888
[hhh=888, null, fff=666, aaa=111, ddd=444, null, null, null, null, ggg=777,
ccc=333, null, null, null, bbb=222, eee=555]
1.6hhh:888
fff:666
aaa:111
ddd:444
ggg:777
ccc:333
bbb:222
eee:555
[hhh=888, null, fff=666, aaa=111, ddd=444, null, null, null, null, ggg=777,
ccc=333, null, null, null, bbb=222, eee=555]
1.7hhh:888
fff:666
aaa:111
ddd:444
ggg:777
ccc:333
bbb:222
eee:555
[hhh=888, null, fff=666, aaa=111, ddd=444, null, null, null, null, ggg=777,
ccc=333, null, null, null, bbb=222, eee=555]
1.8aaa:111
ccc:333
bbb:222
eee:555
ddd:444
ggg:777
fff:666
hhh:888
[aaa=111, null, ccc=333, bbb=222, eee=555, ddd=444, ggg=777, fff=666,
null, hhh=888, null, null, null, null, null, null]

各バージョン取得する順序はバラバラですね。Java SE1.6と1.7が同じ値だったのは意外でした。

また、J2SEとJavaSEで値を取得する順序も違うっていうのは面白いですね。
KeySetで確認しても同じでしたのでIteratorの違いというよりKeySetクラスの仕様が変わったのかな。

まとめ

HashMapは結構手を入れらてるんですね。各バージョンによって同じ値を入れた場合でも取得順は異なるということがわかりました。

JAVAのバージョンアップを行う際は注意したほうがよさそうですね。
(順序保証されていないので順番に取得しようとしてるのはどうかと思いますが。)

-JAVA
-,