bigbully +

HashMap

这篇笔记是为了记录一下HashMap的在jdk中是如何实现的,以前也瞥过几眼代码,自以为有所了解,这次从头到尾看了一遍,发现以前确实还有很多遗漏的地方,这次一并总结出来。

HashMap实现了Map接口那么我们就来看看Map总共提供了哪些接口:

public interface Map<K,V> {

    boolean containsKey(Object key);

    boolean containsValue(Object value);

    V get(Object key);

    V put(K key, V value);

    V remove(Object key);

    void putAll(Map<? extends K, ? extends V> m);

    void clear();

    Set<K> keySet();

    Collection<V> values();

    Set<Map.Entry<K, V>> entrySet();
}

HashMap实现了Map接口,在逐个分析这些关键方法之前,先要来看一下HashMap的容量是怎么确定的,因为这和这些方法的具体实现有很大关系。

 /**
 * The default initial capacity - MUST be a power of two.
 */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

当我们new HashMap的时候,不指定容量的大小,那么会使用这个默认大小。正如注释所说,容量的大小必须是2得n次方,为什么必须是2得n次方?后面会有详尽解释。

当然和所有数据结构的初始化方式一样。我们也可以手动指定初始化的容量:

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

事实上在调用构造方法时时不会对初始化大小做限制的,当我们调用put方法往hashMap中放入键值对时,才会真正对hashMap中的hashTable进行初始化:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {//如果table还未进行初始化,则调用inflateTable方法对table扩容
        inflateTable(threshold);
    }
    if (key == null)//如果key为null则直接调用putForNullKey方法,把value放入table[0]中
        return putForNullKey(value);
    int hash = hash(key);//没有直接用key.hashCode,使用自己的hash函数
    int i = indexFor(hash, table.length);//找到hash值在table中的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {//查找有相同key值(==或equals)则替换为新的value返回旧的value
        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++;//modCount代表hashMap结构变更的次数,当对hashMap进行遍历时遇到另一个线程对hashMap进行了修改,则会fast fail
    addEntry(hash, key, value, i);//添加元素到table[i]中
    return null;
}

这个put方法中包含了很多需要注意的细节,例如inflateTable方法,只有在往map中put键值对时才会真正初始化hashTable:

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);//根据初始化参数传入的size,找到一个2的n次方大于这个size

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//设置threshold的值,为下一次扩容做准备
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

仔细看看roundUpToPowerOf2这个方法,其实逻辑很清晰:

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    int rounded = number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;

    return rounded;
}

如果所给的number大于hashMap容量最大值,则取最大值;否则判断如果如果这个int值得最高位的1的位置不存在的话,即这个int为0的话,则设置rounded为1;否则判断二进制的number存在多少个1,如果只有一个1的话,肯定是2的n次方直接返回,如果不只有一个1的话,左移一位保证rounded为2的n次方,返回。

再来看initHashSeedAsNeeded(capacity)方法

final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;//当前是否使用了altHash
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//是否使用了系统hash,通过内部类Hold在jvm启动时初始化Holder.ALTERNATIVE_HASHING_THRESHOLD
    boolean switching = currentAltHashing ^ useAltHashing;//如果两者有一个为真
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }//则重新初始化hashSeed
    return switching;
}

inflateTable方法大概就是这样,接下来看一下hash函数究竟做了些什么?

为什么需要这个hash函数呢,明明每个key都有其对应的hashCode了。在具体了解hash函数之前先来看一下indexFor方法:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

为什么indexFor方法不是h % length,而是h & (length-1)呢。考虑到hashMap的length永远是2的n次方,h & (length-1)与h % length相等,而位运算显然有更高的效率。

在达成以上共识的情况下,让我们来考虑这样一种情况:

按默认大小初始化一个HashMap,length = 15,然后尝试把key为31、63和95的三个键值对放入map中,结果如何呢?分别对这key值执行h & (length - 1):

31=00000000000000000000000000011111 => 1111=15 63=00000000000000000000000000111111 => 1111=15 95=00000000000000000000000001011111 => 1111=15

得到的结果相同,也就是说这三个数会造成hash冲突。也就是说(2n-1)总会是一个1的序列,因此不管怎样,最后会执行0&1的于然后得出0。上面的例子,也就解释了凡是在末尾全是1111的都返回相同的index,因此,尽管我们有不同的hashcode,对象却都被存在table中index为15的位置。

所以需要hash函数来规避这个问题,下面来看一下hash函数都做了些什么:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

首先会对String类型的key值应用sun.misc.Hashing.stringHash32来获取他的hash值。下面的一系列位运算我只能说根据注释来看会让hashmap的元素能够均匀的分布在hashTable里,降低发生hash冲突的可能性,但具体为什么这个算法能带来这样的效果,我也没有深入研究。

接下来再开看看put方法中最后调用addEntry把元素放入hashTable中是怎么做的:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//如果size超过阈值,并且当前这个key值的位置已经有元素占据了
        resize(2 * table.length);//则进行扩容,容量为之前hashTable的两倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);//如果无需扩容这直接创建entry
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);//new Entry的过程中会把链表中上一个元素和当前元素关联起来,并且把当前元素放置在链表的顶部,出于新添加的元素有可能会更频繁的被访问到的原则
    size++;
}

这样就把put方法的主流成解析完了,下面来看一下resize是怎么做的:

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, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

resize方法没有任何特别之处,直接看看transfer是怎么做的:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {//遍历老得hashTable
        while(null != e) {//如果table中的元素不为null
            Entry<K,V> next = e.next;//暂存e.next
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];//把newTable[i]赋予e.next
            newTable[i] = e;//把e元素放置在newTable[i]上
            e = next;//把e.next赋到e上
        }
    }
}

就这样完成了hashTable中所有元素,包括每个链表中的所有元素的重新分配。