【HashMap源码解读-hash、put、resize】

HashMap是一个基于key做hash的K-V数据结构。

这篇文档会用通俗易懂的方式解读一下HashMap的源码, 看看HashMap底层到底是咋回事。

话不多说, 开整。

先把HashMap的核心结构整出来, 下面标注出了每一个属性和方法都是干啥的, 盘它~

public class HashMap<K,V>...{

	transient Node<K,V>[] table; // 存储数据的数组 (主要就是操作这个玩意)
    transient Set<Map.Entry<K,V>> entrySet; //遍历用的全量数据Set
    transient int size; //数组的大小
    transient int modCount; //修改计数器。fail-fast用的
    int threshold; //阈值, 数组长度达到这个值, 就会考虑扩容
    final float loadFactor; //负载因子

	// 复载了三个构造方法
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认0.75
    }

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

	public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException(...);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException(...);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //查
	public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

	//批量新增
	public void putAll(Map<? extends K, ? extends V> m) {
	        putMapEntries(m, true);
    }

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

	//删
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

	//扩容
	final Node<K,V>[] resize() {
		...
	}

	//树化
	final void treeifyBin(Node<K,V>[] tab, int hash) {
		...
	}

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

}

大体结构, 就是一个Node数组, 用来存K-V。
然后有entrySet/size/modeCount/htreshold/loadfactor这几个属性。
以及三个构造和hash、增、删、改、查、扩容、树化等方法。
还算简单明了。

下面我们主要看一下hash、put、扩容 其他的方法有时间再整理给大家

先从hash方法开始

HashMap为了提升hash效率主要做了两个优化:

  • 使用位运算&代替取模(%)运算来计算下标。
  • 对key的hashcode进行扰动计算。

下面是位运算&计算下标的源码, 我们分析下实现原理。

1、位运算&代替取模(%)运算来计算下标
(n - 1) & hash

n是数组长度, 假设n = 16, hash = 51, 那么下标 = hash & 15 = 3 = 51 % 16
在这里插入图片描述
原理是:
在二进制计算下
hash/16相当于 hash >> 4, 被移除的4位刚好就是余数 hash % 16。
而hash & 15刚好取的就是最后4位。
所以可以讲 hash & 15 等价于 hash % 16。
而且这个方法只对2的n次幂起作用, 换成其他数就不开门了~
所以HashMap初始长度是2的N次幂, 每次扩容也都是2倍的扩。

2、扰动计算

在hash方法中, 高16位和低16位做了个抑或运算, 这里就是扰动计算的实现。为啥这么干呢?

static final int hash(Object key) {
  	int h;
  	// h ^ (h >>> 16)
    // 高16位和低16位做了个抑或运算
   	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我们用大一点的数字举例, 可以看到, 高位上的数字其实是被抹掉了, 没有参与到运算中, 这样会导致, 高位不同低位相同的数发生碰撞, 增大了碰撞的概率, 所以将高低位进行抑或, 这样高低位就都参与到下标计算中, 减小了碰撞和概率。
在这里插入图片描述

看下put方法流程

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

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  boolean evict) {
        // Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 源码写法自带防御属性, 我们改一下, 把他拍散, 方便加注释, 辅助阅读
        
        Node<K,V>[] tab = table; // 就是table数组 
        
        int n = table == null ? 0 : tab.length; //table数组的长度
        
        int i = (n - 1) & hash; // 当前key应该存放位置的角标
        
        Node<K,V> p; // 当前key将要存放位置链表/红黑树上的Node节点(遍历用)
        
        if (tab == null || n  == 0)
        	// 欧吼~ 懒加载哈 
        	// 数组没有初始化, 那就去初始化, resize()是初始化或者扩容
            n = (tab = resize()).length;
        if ((p = tab[i]) == null)
        	// 要插入的位置上没有元素, 直接将K-V包装成Node存进去
            tab[i] = newNode(hash, key, value, null);
        else {
        	// 否则, 说明发生hash碰撞
        	
            Node<K,V> e; // 撞上的那个节点(会遍历赋值, 判断是否要覆盖)
            
            K k = (p == null ? null : p.key);// 撞上的那个节点的key
            
            if (p.hash == hash && (k == key || (key != null && key.equals(k)))){
            	//本次要存的key和链表/红黑树上第一个节点的key相等, 说明重复了, 后面需要做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.next;
                    if (e == null) {
                    	// 当前节点为null, 说明遍历完了。就把当前k-v封装成Node, 追加到链表尾部
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1){
                        	// 链表长度 > 8 进行树化 
							treeifyBin(tab, hash);
						}
                        break;
                    }
                    //当前节点key与本次要存的key相等, 说明重复了, 后面需要做value覆盖
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
						break;
					}
					// 指针往后移动, 指向下一个节点
                    p = e;
                }
            }
            
            if (e != null) {
            	// 发生碰撞, 且key已经在Map中, 进行value覆盖
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null){
                     e.value = value;     
                }
                //模板方法, 留给子类扩展
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //计数器, fail-fast是哟哦那个
        ++modCount;
        if (++size > threshold){
        	//超过阈值, 扩容
            resize();
		}
		//模板方法, 留给子类扩展
        afterNodeInsertion(evict);
        return null;
    }


总结一下就是:

1、先通过key的hash计算索引位置
2、判断该位置为null, 就直接将K-V封装成Node节点存进去
3、如果不为空, 说明有碰撞, 遍历链表, 做K值比较, 如果找到相等的key, 那就将该节点的value覆盖
4、如果没有找到相等的key, 那就存到链表尾部, 然后判断是否需要树化
5、++size后判断是否需要扩容, 若大于阈值就进行扩容

扩容逻辑

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//原数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//元数组大小
        int oldThr = threshold;//扩容阈值
        int newCap = 0;//新数组大小
        int 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){
            	//向左位移1 相当于 *2 
            	//新数组长度 < 最大值 并且 老数组长度 > 默认值  阈值*2
                newThr = oldThr << 1; // double threshold
            }
        } else if (oldThr > 0){
            newCap = oldThr;
        }else { 
        	//初始化 
            newCap = DEFAULT_INITIAL_CAPACITY;//默认16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认12
        }
        
        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) {
        	//遍历老数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //当前角标位置有值,去处理, 无值就算了 
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null){
                    	// 只有一个节点, 未形成链表, 只需要将当前节点重新hash到新数组中
                    	newTab[e.hash & (newCap - 1)] = e;
                    } else if (e instanceof TreeNode){
                    	((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    }  else { 
                    	//存在链表, 遍历链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                        //遍历
                            next = e.next;
                            //当前node的hash与老数组长度取与
                            if ((e.hash & oldCap) == 0) {
                            	//结果=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;
                            //换位置的节点 直接放进 新数组 j + oldCap位置上
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容时, 遍历链表进行迁移, 用了 (e.hash & oldCap) == 0 这个判断, 啥意思呢?
如下图, 当数组长度从16扩大到32的时候, hash & oldCap = 0 的还在原来的位置, 而不等于0 的统一都向钱移动了oldCap的长度,
所以用这种方式就可以简单快速的将链表拆分成2部分, 一部分待在原来的位置, 另一部分移到新的位置, 不需要新老位置都进行遍历插入, 效率得到了很大提升。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值