HashMap源码分析

感谢 shixinzhang的文章, 参考此: https://blog.csdn.net/u011240877/article/details/53351188 文章

一. HashMap的12个成员变量含义:

/**

* 初始容量为16

*/

static final int DEFAULT_INITIAL_CAPACITY =1 <<4; // aka 16

/**

* 最大容量为2的三十次方

*/

static final int MAXIMUM_CAPACITY =1 <<30;

/**

* 加载因子: 0.75f

* 分成四等份, 0.25 , 0.25 * 3 = 0.75, 在容量3/4时(0.75)进行扩容

*/

static final float DEFAULT_LOAD_FACTOR =0.75f;

/**

* 1. 当前槽位Entry(也就是Node节点数 >= TREEIFY_THRESHOLD此值, 并且当前table数组的长度 >= MIN_TREEIFY_CAPACITY, 则将链表转成红黑树.

* 2. 当前槽位Entry(也就是Node)节点数 >=TREEIFY_THRESHOLD,  并且当前table数组的长度 < MIN_TREEIFY_CAPACITY, 则进行扩容,不发生树化

*/

static final int TREEIFY_THRESHOLD =8;

/**

* 当前槽位Entry(也就是Node)节点数小于等于6时, 由红黑树转成链表

*/

static final int UNTREEIFY_THRESHOLD =6;

/**

* 最小的元素容量, 结合 TREEIFY_THRESHOLD 使用, 判断什么转成树和扩容

*/

static final int MIN_TREEIFY_CAPACITY =64;

/**

*哈希表中的链表数组

*/

transient Node[] table;

/**

*键值对集合

*/

transient Set> entrySet;

/**

* 键值对

*/

transient int size;

/**

* 当前 HashMap 修改的次数,这个变量用来保证 fail-fast 机制

fail-fast 机制 : https://blog.csdn.net/zymx14/article/details/78394464

*/

transient int modCount;

/**

* The next size value at which to resize (capacity * load factor).

* 阈值, 下一次扩容的值(容量*负载系数)

int threshold;

/**

* 哈希表的加载因子

*/

final float loadFactor;

HashMap本身就是Entry数组,每个槽位就是第一个Entry节点,下一个节点就是由前一个 next指向下一个节点, 所以同一个链表的hash相同


二. HashMap 的初始容量和加载因子

由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:

    1.容量:数组的数量

    2. 加载因子:决定了 HashMap 中的元素占有多少比例时扩容

成为了 HashMap 最重要的部分之一,它们决定了 HashMap 什么时候扩容。

HashMap 的默认加载因子为 0.75,这是在时间、空间两方面均衡考虑下的结果:

    1. 加载因子太大的话发生冲突的可能就会大,查找的效率反而变低

    2. 太小的话频繁 rehash,导致性能降低

当设置初始容量时,需要提前考虑 Map 中可能有多少对键值对,设计合理的加载因子,尽可能避免进行扩容。

如果存储的键值对很多,干脆设置个大点的容量,这样可以少扩容几次。

三. HashMap四个构造方法

    public HashMap(int initialCapacity, float loadFactor) {

        if (initialCapacity < 0)

            throw new IllegalArgumentException("Illegal initial capacity: " +

                                              initialCapacity);

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        if (loadFactor <= 0 || Float.isNaN(loadFactor))

            throw new IllegalArgumentException("Illegal load factor: " +

                                              loadFactor);

        this.loadFactor = loadFactor;

        //根据指定容量设置阈值

        this.threshold = tableSizeFor(initialCapacity);

    }

// 这个阈值经过 无符号右移、求异运算;

static final int tableSizeFor(int cap) {

    int n = cap - 1;

    n |= n >>> 1;

    n |= n >>> 2;

    n |= n >>> 4;

    n |= n >>> 8;

    n |= n >>> 16;

    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

    public HashMap(int initialCapacity) {

        this(initialCapacity, DEFAULT_LOAD_FACTOR);

    }

    public HashMap() {

        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

    }

    public HashMap(Map m) {

        this.loadFactor = DEFAULT_LOAD_FACTOR;

        putMapEntries(m, false);

    }

/**

* 向哈希表中添加整个集合

*/

final void putMapEntries(Map m, boolean evict) {

int s = m.size();

    if (s >0) {

if (table ==null) {// pre-size

            float ft = ((float)s / loadFactor) +1.0F;

            int t = ((ft < (float)MAXIMUM_CAPACITY) ?

(int)ft : MAXIMUM_CAPACITY);

            if (t > threshold)

threshold = tableSizeFor(t);

        }

// 数组不为空, 超过阈值,则进行扩容

        else if (s > threshold)

resize();

        for (Map.Entry e : m.entrySet()) {

K key = e.getKey();

            V value = e.getValue();

            // copy添加集合的值

            putVal(hash(key), key, value, false, evict);

        }

}

}

五.  链表节点Node

static class Nodeimplements Map.Entry {

final int hash; // 哈希值,

    final K key; // 键

    V value; // 值

    Node next; // 指向下一个node

    Node(int hash, K key, V value, Node next) {

this.hash = hash;

        this.key = key;

        this.value = value;

        this.next = next;

    }

public final K getKey()        {return key; }

public final V getValue()      {return value; }

public final String toString() {return key +"=" + value; }

public final int hashCode() {

return Objects.hashCode(key) ^ Objects.hashCode(value);

    }

public final V setValue(V newValue) {

V oldValue = value;

        value = newValue;

        return oldValue;

    }

public final boolean equals(Object o) {

if (o ==this)

return true;

        if (oinstanceof Map.Entry) {

//  Map.Entry 相等的条件: 键相等, 值相等, 个数相等, 顺序相等.

            Map.Entry e = (Map.Entry)o;

            if (Objects.equals(key, e.getKey()) &&

Objects.equals(value, e.getValue()))

return true;

        }

return false;

    }

}


六. putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

              boolean evict) {

// 如果当前table为空, 则新建; n:指向最后一个桶的位置; tab: 新哈希表

    Node[] tab; Node p; int n, i;

    if ((tab = table) ==null || (n = tab.length) ==0)

n = (tab = resize()).length;

    // 如果要插入的位置没有元素, 新建个节点放进去

    if ((p = tab[i = (n -1) & hash]) ==null)

tab[i] = newNode(hash, key, value, null);

    else {

// 如果要插入的桶已经有元素,替换

// e : 指向被替换的元素

        Node e; K k;

        if (p.hash == hash &&

((k = p.key) == key || (key !=null && key.equals(k))))

// p: 指向要插入的桶第一个元素的位置, 如果p 的哈希值,键,值和要添加的一样, 就停止找, e指向p;

            e = p;

        else if (pinstanceof TreeNode)

// 如果桶第一个元素是树形node, 则在树中插入

            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

        else {

// 进行链表查找,替换

            for (int binCount =0; ; ++binCount) {

// 如果下个元素为空, 则插入后面

                if ((e = p.next) ==null) {

p.next = newNode(hash, key, value, null);

                    if (binCount >= TREEIFY_THRESHOLD -1)// -1 for 1st

// 当这个桶内链表个数大于等于8, 就要树形化

                        treeifyBin(tab, hash);

break;

                }

// 如果找到要替换的节点, 就停止,

                if (e.hash == hash &&

((k = e.key) == key || (key !=null && key.equals(k))))

break;

                p = e;

            }

}

// 存在要替换的节点

        if (e !=null) {// existing mapping for key

            V oldValue = e.value;

            // 替换, 返回

            if (!onlyIfAbsent || oldValue ==null)

e.value = value;

            afterNodeAccess(e);

            return oldValue;

        }

}

++modCount;

    // 如果超过阈值, 扩容

    if (++size > threshold)

resize();

    afterNodeInsertion(evict);

return null;

}

添加方法的逻辑概括为:


七.  计算hash()方法

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

// 右移位是去掉低位, 然后异或之后, 高低位结合, 减少碰撞率;

八.resize() 扩容方法

当集合所有元素 > threshold 时;

final Node[] resize() {

// 复制一份当前的数据

    Node[] 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)

// 如果旧容量大于等于16, 新的阈值就是旧阈值的两倍

            newThr = oldThr <<1; // double threshold

    }

// 如果旧容量为0, 并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值

    else if (oldThr >0)// initial capacity was placed in threshold

        newCap = oldThr;

    else {// zero initial threshold signifies using defaults

// 旧容量,旧阈值都是0,说明还没有创建哈希表,容量为默认容量,阈值为 容量*加载因子

        newCap = DEFAULT_INITIAL_CAPACITY;

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

    }

// 如果新的阈值为0, 就得用 新容量 * 加载因子

    if (newThr ==0) {

float ft = (float)newCap * loadFactor;

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

    }

// 更新阈值

    threshold = newThr;

    // 创建新链表数组, 容量是原来的两倍

    @SuppressWarnings({"rawtypes","unchecked"})

Node[] newTab = (Node[])new Node[newCap];

    table = newTab;

    // 接下来就得遍历复制了

    if (oldTab !=null) {

for (int j =0; j < oldCap; ++j) {

Node e;

            if ((e = oldTab[j]) !=null) {

// 旧的桶置为空

                oldTab[j] =null;

                // 当前 桶只有一个元素, 直接赋值给对应的位置

                if (e.next ==null)

newTab[e.hash & (newCap -1)] = e;

                else if (einstanceof TreeNode)

// 如果旧哈希表中这个位置的桶是树形, 把新哈希表里当前桶也变成树形

                    ((TreeNode)e).split(this, newTab, j, oldCap);

                else {// preserve order

// 保留旧哈希表桶中链表的顺序

                    Node loHead =null, loTail =null;

                    Node hiHead =null, hiTail =null;

                    Node next;

                    do {

next = e.next;

                        if ((e.hash & oldCap) ==0) {

if (loTail ==null)

loHead = e;

else

                                loTail.next = e;

                            loTail = e;

                        }

else {

if (hiTail ==null)

hiHead = e;

else

                                hiTail.next = e;

                            hiTail = e;

                        }

}while ((e = next) !=null);

                    if (loTail !=null) {

loTail.next =null;

                        newTab[j] = loHead;

                    }

if (hiTail !=null) {

hiTail.next =null;

                        newTab[j + oldCap] = hiHead;

                    }

}

}

}

}

return newTab;

}

扩容过程中几个关键的点:

新初始化哈希表时,容量为默认容量,阈值为 容量*加载因子

已有哈希表扩容时,容量、阈值均翻倍

如果之前这个桶的节点类型是树,需要把新哈希表里当前桶也变成树形结构

复制给新哈希表中需要重新索引(rehash),这里采用的计算方法是

e.hash & (newCap - 1),等价于 e.hash % newCap

结合扩容源码可以发现扩容的确开销很大,需要迭代所有的元素,rehash、赋值,还得保留原来的数据结构。

所以在使用的时候,最好在初始化的时候就指定好 HashMap 的长度,尽量避免频繁 resize()。

你可能感兴趣的:(HashMap源码分析)