HashMap分析

hash函数

// 1.7
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

// 1.8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个函数也叫扰动函数,它的作用就是解决hash冲突的。

indexFor函数

获取hash时真正的索引值。这个函数也叫索引函数

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

扰动函数和索引函数的作用

索引函数的作用就是实际的找到value值存储的位置,这里的运算是“与”运算,这个的效率更高,相对于“取模”运算,这个的运算消耗更小。

这里是hash数组的length-1,这个正好是数组的实际容纳容量,并且由于数组的大小都是2的指数,减1能在地位产生更多的1,正好相当于一个低位掩码

那扰动函数的作用是啥?

由于“与”操作的结果是高位全部归零,只保留了低位的值。这样的问题就来了,由于不管我的h值是怎样的,最后只取最低的几位(由数组的大小决定),这样碰撞还是很严重的。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。

hash函数的输入值是int,如果啥都不做的话,基本也是可以用的,因为int值的范围从-2147483648到2147483648,但是由于与操作的问题,需要加入扰动函数。

1.7和1.8的扰动函数有所不同,但是它们的目的是一样的,1.8更加的简单化。就是混淆原始哈希码的高位和地位,以此来加大低位的随机性,或者说低位的值也反映了高位的特征,高位的信息被变相的保留下来,而不担心后面与操作直接被忽略调

以1.8为例:

扰动函数的效果怎么样呢?

随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。

可以看出,随着掩码位数的减少,扰动函数的作用就更为明显。

构造

hashmap提供了多种构造方法,这里主要分析:

// 1.7
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

// 1.8
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

Entry结构体

// 1.7
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ...
}

// 1.8
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

从上面看,1.7和1.8没有什么差异,都是一个链的概念。

Resize计算

1.7的大小计算:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

1.7的resize就是把数组变大,计算就是原始容量乘上加载因子loadFactor;然后将原来的table里的数据放入到新的数组里。

1.8的大小计算

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 直接增大2倍
        }
        else if (oldThr > 0) // 设定了初始的threshold
            newCap = oldThr;
        else {               // 初始化容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 类似1.7的容量大小增长
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
           // 红黑树操作
           ... ...
        }
        return newTab;
}

可以看出,1.8的容量扩充更为复杂,分3种情况:

  • 通过空的构造体实例化的hashmap,没有任何的数据,则直接初始化容量
  • 已经有了数据,则直接扩大2倍
  • 没有数据,但是threshold已经设置了值,则按1.7的增长算法进行扩容

在扩容后,数据是通过红黑树的方式添加到新的数组中的。

冲突处理(基于1.7)

1.7的内部存储结构采用的是数组链表的结合。

链表的出现主要是为了解决hash地址冲突的问题。

// 储存 key-value 键值对的数组,一个键值对对象映射一个Entry对象
transient Entry[] table;

具体实现

系统总是将新添加的Entry对象放入table数组的bucketIndex索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

存在的性能问题

HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,查询时间为O(1);get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素,这时时间消耗为O(N)。这就是链表解决冲突后出现的问题。

1.8的优化处理

1.8对上面问题提出了解决方案,就是在链表的基础上采用红黑树的方式。具体看HashMap java 8分析

补充知识 - hashmap冲突的处理方式

  • 开放地址法,即再散列
  • 链地址法
  • 伪随机数

存储

1.7和1.8的存储逻辑基本一样:

  • 获取key的hashcode
  • 二次hash
  • 通过hash找到对应的index
  • 插入链表
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;
    }

void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

读取

  • bucket里的第一个节点,直接命中;
  • 如果有冲突,则通过key.equals(k)去查找对应的entry
    • 若为树(1.8),则在树中通过key.equals(k)查找,O(logn);
    • 若为链表(1.7),则在链表中通过key.equals(k)查找,O(n)。
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

results matching ""

    No results matching ""