【Explore SRC】一起看看HashMap源码(个人笔记)

HashMap源码一直是众多Java程序员的必经之路,今天我也看看,大家凑热闹不?基于水平有限,有些地方理解错误、理解不了,请大家指出哦~~

 

版本说明

查看的版本是jdk1.7.0_71

 

结构概要图

 

从构造方法看起吧

public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)

HashMap有4个构造方法,具体看下代码,可知第2、3个方法都是调用第1个方法进行操作的。那么,具体看第1个吧。

 

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

查看参数的全局变量,知道初始化容量是16,扩容因子(容量达到哪里时要重新构造HashMap的容器)默认为0.75。

最后具体看第1个方法的方法体,主要作了3件事:

1、如果入参异常,则抛出异常

2、对初始化容量进行饱顶

3、将入参设置为属性,这里有点注意:threshold(阀值),在HashMap刚初始化时被赋值为初始容量。

4、后面,还调用了init(),此方法是空方法体的,供子类的开发人员扩展

 

哪些方法常用,当然是上子弹的方法了--put(K key, V value)

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

......
/** * Inflates the table. */ private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }

内部有个叫table的数组,其元素指向的是单向链表,而这链表装载的是Entry类(内部类,实现Map.Entry),实际主要包含每个元素的数据结构,比如key、value、next指针等。

刚刚初始化HashMap时,此时table为空,这时就需要根据threshold对table进行扩容。

将table扩容至threshold的上随2的n次方大小。比如,threshold为16,则扩容至16;threshold为17,则扩容至32。

注:

roundUpToPowerOf2()见下述。

 

if (key == null)
    return putForNullKey(value);

查看putForNullKey方法,将key为null的元素,放入table下表为0的链表里。而逻辑与下面要讲的放入元素的逻辑基本一致。

注:

为什么是下标为0的元素放key为null的值呢?见下述。

 

int hash = hash(key);
int i = indexFor(hash, table.length);

对key对象进行哈希计算后,映射到table数组中一个位置,为i。

注:

indexFor(),见下述。

 

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;
    }
}

上面已找到当前要插入的元素位于table数组的哪个位置了,接下来就线性遍历这个位置指向的链表,如果发现hash值相等并且key也相等的,就说明此Map已包含此元素,那么,就用新值覆盖旧值,并返回旧值吧。

 

modCount++;
addEntry(hash, key, value, i);

...

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

/**
 * Like addEntry except that this version is used when creating entries
 * as part of Map construction or "pseudo-construction" (cloning,
 * deserialization).  This version needn't worry about resizing the table.
 *
 * Subclass overrides this to alter the behavior of HashMap(Map),
 * clone, and readObject.
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

程序跑到这里,说明在Map中并没有找到Key值,需要作插入。

modCount是记录插入的次数,估计用作限制并发操作的。

addEntry()在插入元素前,要判断元素是否达到一个阀值,如果达到,就对table进行2倍的扩容、重新哈希。(此点内容下面讲述)

然后,重新计算元素在扩容后的位置,调用createEntry()作实际的插入操作。插入操作,就是将新插入的元素的next指向链表的第一个元素,然后将table数字的该下表指向新插入的元素。

 

/**
 * Rehashes the contents of this map into a new array with a
 * larger capacity.  This method is called automatically when the
 * number of keys in this map reaches its threshold.
 *
 * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE.
 * This has the effect of preventing future calls.
 *
 * @param newCapacity the new capacity, MUST be a power of two;
 *        must be greater than current capacity unless current
 *        capacity is MAXIMUM_CAPACITY (in which case value
 *        is irrelevant).
 */
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);
}

/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> 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;
            e = next;
        }
    }
}

先根据newCapacity实例化一个新的table。

因新table的长度变更了嘛,需遍历原table所指向的链表的所有元素,一个个转到新的table(计算hash、重新定位)。(至于是否重新hash,我还没看明白)

 

细节

roundUpToPowerOf2(int number)

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

 

这个方法是计算number最接近的2的N次方数。

其中Integer.highestOneBit()是取最高位1对应的数,如果是正数,返回的是最接近的比它小的2的N次方;如果是负数,返回的是-2147483648,即Integer的最小值。

那为什么要先减1,再求highestOneBit()?

举几个数的二进制就知道了:

00001111 = 15 -> 00011110 = 30 -> highestOneBit(30) = 16

00010000 = 16 -> 00100000 = 32 -> highestOneBit(32) = 32

所以,为了获取number最接近的2的N次方数,就先减一。

附一个简单的分解计算:

public class Lefter {

    public static void main(String[] args) {
        for (int i = 2; i <= 17; i++) {
            System.out.println(i);
            System.out.println(i - 1);
            System.out.println((i - 1) << 1);
            System.out.println(Integer.highestOneBit((i - 1) << 1));
            System.out.println("result : " + i + " -> " + Integer.highestOneBit((i - 1) << 1));
        }
    }

}
View Code

结果:

2
1
2
2
result : 2 -> 2
3
2
4
4
result : 3 -> 4
4
3
6
4
result : 4 -> 4
5
4
8
8
result : 5 -> 8
6
5
10
8
result : 6 -> 8
7
6
12
8
result : 7 -> 8
8
7
14
8
result : 8 -> 8
9
8
16
16
result : 9 -> 16
10
9
18
16
result : 10 -> 16
11
10
20
16
result : 11 -> 16
12
11
22
16
result : 12 -> 16
13
12
24
16
result : 13 -> 16
14
13
26
16
result : 14 -> 16
15
14
28
16
result : 15 -> 16
16
15
30
16
result : 16 -> 16
17
16
32
32
result : 17 -> 32
View Code

 

indexFor(int h, int length)

将h映射到length的范围里,效果就像求模。

return h & (length-1);

将h和length - 1和操作就可以了。

比如length为16,那么:

16 = 00010000

15 = 00001111

 

为什么是下标为0的元素放key为null的值呢?

根据上述indexFor(int h, int length)映射的范围在1到length - 1,那么剩下的下标就是0。

 

为什么hash数组的长度要弄成2的N次方?

要将散列值映射到一定范围内,一般来说有2种方法,一是求模,二是与2的N次方值作&运算。而现代CPU对除法、求模运算的效率不算高,所以用第二种方法会效率比较高,所以数组被设计为2的N次方。

 

剩下的仍未想明白

1、initHashSeedAsNeeded(capacity)

2、hash()

 

posted @ 2016-02-22 15:11  nick_huang  阅读(524)  评论(0编辑  收藏  举报