HashMap源码分析

HashMap源码分析



一、HashMap成员变量

// HashMap的默认初始化容量,左移四位 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大的哈希表容量 左移30位 = 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的负载因子 0.75,使用泊松分布算法根据时间与空间的利用,取折中值0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/*
桶中链表长度转换红黑树的阈值,链表长度超过8的时候,链表会转换为红黑树。
设置这个阈值的目的有两个:
1. 避免链表过长,查找效率降低。转换成红黑树可以提高查找效率。
2. 如果阈值过小,链表转换成红黑树的次数会更多,反而影响性能。阈值8是一个经验值。
*/
static final int TREEIFY_THRESHOLD = 8;

/*
当在resize时,若一个红黑树中的节点数少于该值,会将红黑树重新转换为链表。
这个阈值的设置也有两个考虑:
1. 避免红黑树规模太小,维护代价太大。
2. 阈值要小于TREEIFY_THRESHOLD,否则树化和退化会频繁转换。
所以UNTREEIFY_THRESHOLD的作用是设置红黑树转换回链表的阈值。退化可以避免红黑树太小的维护开销。
*/
static final int UNTREEIFY_THRESHOLD = 6;

/*
当桶中的节点数超过TREEIFY_THRESHOLD(默认为8)时,若当前table的大小< MIN_TREEIFY_CAPACITY,会优先进行扩容,而不是树化。
这个设定的考虑是:
1. 当表较小时,哈希冲突可以通过扩容来解决。
2. 避免表较小时就树化,造成红黑树过多,反而降低效率。
所以,MIN_TREEIFY_CAPACITY用于设定进行树化的最小容量阈值,与扩容配合来优化冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;

/*
存储HashMap中的键值对数据。
table是一个Node<K,V>类型的数组,每个Node表示一个键值对。
*/
transient Node<K,V>[] table;

/*
记录当前HashMap中包含的键值对数量。
size字段用于快速获取Map中的个数,而不需要每次遍历计算,可以优化速度。
*/
transient int size;

/*
用于记录HashMap结构变化的次数,如put、remove等操作。主要用于迭代时的快速失败(fail-fast)机制。
*/
transient int modCount;

/*
用于存储HashMap的阈值,当HashMap中的键值对数量达到threshold时就会进行扩容resize操作。threshold = capacity * loadFactor。
*/
int threshold;

/*
该字段为HashMap的负载因子,用于控制HashMap的密度。loadFactor越大密度越高,冲突的机会加大。默认值为0.75,是在时间和空间成本上做的一个平衡选择。
*/
final float loadFactor;

二、构造方法

1. 无参构造方法

// 其他所有的参数都为默认
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2. 有参构造方法

开放了4个有参构造方法,值得注意的是初始化容量的有参构造方法,不会将HashMap中的容量设定为传入的大小,会进过一系列的位运算后,将传入的容量大小计算为大于或等于传入容量大小的2的幂数值

// 输入初始容量的构造方法。这里的初始参数还需要进行计算为2的幂数值
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 输入初始容量、负载因子的构造方法。
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);
}

/*
计算出大于或等于输入capacity的最小的2的幂数值
原理是利用位运算实现的:
1. 将输入值cap减1,赋值给n。
2. 将n右移1位,然后与原n进行“或”运算。这将保证结果为偶数。
3. 重复右移并“或”运算,直到将所有的1位都置为1。这实际上是计算大于等于cap的最小的2的幂。
4. 如果计算结果小于0,直接返回1。如果结果大于最大容量,返回最大容量。否则返回n+1作为结果。
所以tableSizeFor()实际上是计算一个rounding operation,将输入数值上取到大于等于该值的最小的2的幂数。
这样可以保证HashMap的容量总是2的幂,这对于提高哈希的效率很关键。同时也符合HashMap的扩容机制。
*/
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;
}

// 传参为map的构造方法,先将传入的map中的成员变量计算到新的HashMap中,并且遍历传入的map,使用putVal方法将各个参数复制到新的HashMap中。
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

三、put方法

1. put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

2. hash方法

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

HashMap中的hash方法并不是单纯的使用hashCode()方法直接返回对应的hash值,而是会将返回的hash值的高16位与hash值的低16位使用异或位运算进行扰动,使hash值更加的离散,提高散列性,减少hash冲突。

翻看源码可以提出以下问题:

  1. 为什么不直接使用hashCode()方法生成的hash值来使用,还要进行运算?
  2. 为什么要使用hash值的高16位于hash值的地16位进行扰动,或者说为什么是右移16位?
  3. 为什么要使用异或位运算扰动,而不使用与运算、或运算?
  4. 扰动之后为什么可以提高散列性?
  • 针对第1、2的问题:
    首先是hash值,hash值是int类型,4个字节,32位。所以低16位的变化区间在 0 ~ 2^16-1 这个区间,而高16位的变化区间在 -2^32 ~ 2^32-1 这个区间,那么参与计算的hash值是低16位参与的更多,如果不进行扰动,让高16位不参与运算,会导致散列程度不高。(例如两个hash值的低十六位是一样的,而高十六位不一样,如果直接计算的话,那么数组下标会相同,如果这时候高位与低位异动之后,高位也参与运算,这时候数据分布更加均匀)。所以需要将hash值的高16位与低16位进行运算、扰动,让高16位于参与到运算中,减少hash冲突,防止大量kv值都在一个个下标数组中。

  • 针对第3的问题:
    使用异或位(异或,相同为0,不同为1),由于右移16位之后的hash值高16位都为0(如果使用与运算,那么计算后的hash值高16位全为0,高16位特征全无),使用异或运算,计算之后的hash值会保留原来hash值的高16位特征。同时右移之后的低16位中有1则会参与异或运算中扰动,如果这时候使用的是或运算,当原hash值的低16位全为1时,那么运算之后的hash值相当于没有扰动成功,还是会出现hash冲突的情况。

综上所述,可以解答第4问。

3. putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 给tab赋值,如果当前数组为空或者长度位0,那么需要初始化数组,并返回数组的大小n
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 计算出当前要put的数据的数组下标,找到数组对应的元素位置,
    // 并赋值给p,判断当前数组位置元素是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 当前数据为空,那么将输入的数据生成新的node对象,赋值到对应的元素位置。
        tab[i] = newNode(hash, key, value, null);
    else {
    	// 如果当前数组存在元素的情况。
        Node<K,V> e; K k;
        // 如果当前数组位置上元素的hash等于传入数据的hash值,同时key相等。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 把原有的元素赋值给e,判断跳出,后续做value值更新处理。
            e = p;
        else if (p instanceof TreeNode)
        	// 如果当前节点是树节点,那么将传入数据设置为树节点后加入到该红黑树中。
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 当前节点为链表遍历链表
            for (int binCount = 0; ; ++binCount) {
            	// 将当前节点的下节点赋值给e,并判断下节点是否为空,为空那么将传入的数据设置为当前p节点的下节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 设置完节点之后,除去已设置的节点,当前链表的数量为binCount+1,如果大于或等于8,那么需要设置树节点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	// 设置树节点,里面会判断该tab数组长度是否大于MIN_TREEIFY_CAPACITY=64,
                    	// 如果大于才会将当前hash对应的节点设置为树节点,否则只会使用resize()方法进行扩容
                        treeifyBin(tab, hash);
                    // 跳出循环,这里e==null
                    break;
                }
                // 如果e(当前节点的下节点)不为null,且hash、key与当前e的相同,那么跳出循环。后续处理赋值操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 未赋值前,e为p的下节点,这时候将e复制给p,那么
                p = e;
            }
        }
        // e不为空情况:1.当前节点为树节点
        // 2. 当前节点数据和传入的数据的数组下标相同以及key相同。
        if (e != null) { // existing mapping for key
        	// 将旧节点的value值赋值给oldValue
            V oldValue = e.value;
            // onlyIfAbsent 为true的话,当前节点有value值,那么不会替换新value值
            if (!onlyIfAbsent || oldValue == null)
            	// 将新的value值赋值给当前节点。
                e.value = value;
            afterNodeAccess(e);
            // 返回旧节点中的value
            return oldValue;
        }
    }
    // 只有当链表、树节点增加或者删除了节点才会+1
    ++modCount;
    // 新节点插入之后,size+1,且判断是否超过扩容阈值,超过那么需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

4. treeifyBin()方法

// 链表转红黑树方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果传入的节点数组为空,或者传入的节点数组长度小于64,那么会使用resize方法进行扩容。
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 根据hash找到对应的链表,不为null则开始树化。
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 遍历链表,将链表中每个节点元素都转换成树节点。
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                // 第一个树节点元素赋值给hd
                hd = p;
            else {
                // 除了第一个树节点元素,其他树节点设置前一节点、后一节点,组成双向链表。
                p.prev = tl;
                tl.next = p;
            }
            // 将当前树节点p赋值给tl,未赋值前tl为前一树节点。
            tl = p;
        } while ((e = e.next) != null);// 节点元素下一节点不为null,继续循环。
        // 将设置好的红黑树链表hd赋值到节点数组中。覆盖原来的链表。如果不为空,还需要对红黑树节点组织成红黑树结构。
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

四、 resize数组初始化、扩容

1. resize()

final Node<K,V>[] resize() {
    // 获取当前节点数组,复制给oldTab为老数组
    Node<K,V>[] oldTab = table;
    // 设置老节点数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 设置老节点数组的阈值。
    int oldThr = threshold;
    // 初始化新数组长度、新数组阈值
    int newCap, newThr = 0;
    // 如果老数组的长度大于0
    if (oldCap > 0) {
        // 如果老数组长度大于等于节点数组的最大值 2^30
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 那么老数组阈值设置为int的最大值 2^32-1
            threshold = Integer.MAX_VALUE;
            // 并返回老节点数组
            return oldTab;
        }
        // 如果老数组长度左移一位,即两倍,赋值给新数组(这里相当于将数组长度扩容了2倍)
        // 并且小于数组长度最大值以及老数组长度大于默认的16.
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 老节点数组阈值长度左移一位,即两倍,然后复制给新数组阈值。(数组扩容两倍、阈值扩容两倍)
            newThr = oldThr << 1; // double threshold
    }
    // 如果老数组长度=0且老数组阈值大于0
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 新节点数组长度就等于老数组的阈值
        newCap = oldThr;
    // 老数组长度、阈值都为0
    else {               // zero initial threshold signifies using defaults(这里是初始化数组)
        // 初始化数组长度为默认的16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 初始化新数组阈值为默认负载因子0.75X默认初始化容量大小16 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新数组阈值等于0
    if (newThr == 0) {
        // 那么新数组长度X负载因子得到ft(初始阈值)
        float ft = (float)newCap * loadFactor;
        // 根据计算出来的新数组初始阈值判断,如果新数组长度、计算得到的初始阈值小于容量最大值,那么新数组阈值为初始阈值ft
        // 否则新数组阈值为int的最大值 2^32-1
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 将新数组阈值复制给当前HashMap阈值属性。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 根据计算出来的新数组大小创建一个新的节点数组。
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 创建出来的新节点数组赋值给table。
    table = newTab;
    // 如果老数组不为null,这里需要将老数组中的元素转移到新数组中。
    if (oldTab != null) {
    	// 遍历老数组上的节点元素
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 把当前下标数据赋值给e,数组上某一下标有数据,那么开始转移数据到新数组中。
            if ((e = oldTab[j]) != null) {
            	// 当前下标的数据设置为0。
                oldTab[j] = null;
                // 判断当前节点数据的下节点是否为空,为空就说明当前节点是单一数据,不是链表。
                if (e.next == null)
                	// 单一节点数据的hash与新数组长度-1进行与运算,算出该节点数据在新数组的下标,并赋值。
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)// 如果当前节点是树节点
                	// 那么需要将当前树节点拆分后赋值给新数组。
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order    当前节点是链表的情况。
                	// 设置一个低位头节点、低位尾节点。
                    Node<K,V> loHead = null, loTail = null;
                    // 设置一个高位头节点、高位尾节点。
                    Node<K,V> hiHead = null, hiTail = null;
                    // 设置当前节点的下个数据节点。
                    Node<K,V> next;
                    do { // 遍历链表
                    	// 当前节点数据的下节点数据赋值给next。
                        next = e.next;
                        // 低位节点、高位节点判断:例如现在老数组长度16,那么计算下标的时候就是hash & (16-1),
                        //这时候节点只有低4位参与与运算,能够组成链表,那么说明该链表上的节点hash低4位都是一样的。
                        // 这时候数组长度翻倍变成32,那么计算节点在新数组的下标就变成hash & (32-1),
                        // 这时候就是低5位参与与运算,由于低4位都是一样的,所以节点上第低5位为1,
                        // 则在新数组下标有变化(还可以知道当前变化位刚好就是老数组长度,所以变化的下标为老数组下标+老数组长度),为0则下标无变化。
                        // 可以反推,只有低第5位有变化,那么在新数组下标有变化,所以hash与老数组长度为0,那么无变化,为低位节点。
                        
                        // 如果当前节点数据hash与老数组长度进行与操作等于0,
                        // 那么说明当前节点数据在新数组的下标和老数组的下标是一样的,那么设置为低位节点。否则为高位节点。
                        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);// 判断下一节点不为null,那么继续循环。
                    if (loTail != null) { 
                    	// 低位尾节点不为null,那么在新数组下标和老数组下标一致,将节点赋值到新数组对应的下标节点中。
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                    	// 高位尾节点不为null,那么在新数组下标是老数组下标+老数组长度。然后将节点赋值到新数组对应的下标节点中。
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2. split()方法,拆分树节点

// map当前hashmap
// tab:老节点数据数组
// index:老节点数组下标
// bit:老数组容量大小
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    // 将当前树节点设置为b
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    // 设置低位头树节点、尾树节点;高位头树节点、尾树节点。
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    // 设置低位树节点长度lc,高位树节点长度hc
    int lc = 0, hc = 0;
    // 遍历树节点
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
    	// 设置当前树节点e的下树节点为next。
        next = (TreeNode<K,V>)e.next;
        // 设置e的下树节点为null
        e.next = null;
        // 当前树节点hash与老数组大小进行与运算,如果为0,说明为低位树节点。
        if ((e.hash & bit) == 0) {
        	// 如果当前树节点的上树节点为null
            if ((e.prev = loTail) == null)
            	// 那么设置当前树节点为低位头树节点。这时候低位头树节点、尾树节点都是e
                loHead = e;
            else
            	// 设置当前树节点e为低位尾树节点的下树节点。(这里开始拼接树节点链表。)
                loTail.next = e;
            loTail = e;
            ++lc;// 计算低位树节点链表的长度。
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    // 如果低位树节点不为null
    if (loHead != null) {
    	// 判断低位树节点长度是否小于或等于树节点退化长度6
        if (lc <= UNTREEIFY_THRESHOLD)
        	// 那么将当前树节点链表遍历设置为节点node后赋值到新数组对应下标中。
            tab[index] = loHead.untreeify(map);
        else {// 当前树节点长度大于树节点退化长度6.
        	// 直接将对应的树节点赋值到新数组的对应下标中。
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
            	// 这里判断高位树节点是否为null,如果不为null,说明低位树节点的不是原来的整条树节点,有变化,所以需要重新生成树结构。
                loHead.treeify(tab);
        }
    }
    // 与低位树节点同,不同的是对应的新数组下标+老数组长度。
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值