zoukankan      html  css  js  c++  java
  • HashMap中resize()剖析

    HashMap中resize()剖析

    final Node<K,V>[] resize() {
    	Node<K,V>[] oldTab = table;
    	int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	int oldThr = threshold;
    	int newCap, newThr = 0;
    	// 只有非第一次扩容才会进来(第一次扩容在第一次put)
    	if (oldCap > 0) {
    		// oldCap最大为MAXIMUM_CAPACITY(2^30),可查看带参构造方法①
    		if (oldCap >= MAXIMUM_CAPACITY) {
    			 /**
                     * threshold变成MAX_VALUE(2^31-1),随它们碰撞。但是oldCap不改变,
                     * 因为如果oldCap翻倍就为负数了,如果赋值为MAX_VALUE,
                     * 参考 Map容量为什么不能为MAX_VALUE②
                     */
    			threshold = Integer.MAX_VALUE;
    			return oldTab;
    		}
    		// 容量翻倍
    		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    				 oldCap >= DEFAULT_INITIAL_CAPACITY)
    			/**
                 * 为什么需要判断oldCap >= DEFAULT_INITIAL_CAPACITY呢?
                 * 应该是容量较小时 capacity * loadFactor造成的误差比较大,
                 * 例如初始化容量为2 threshold则为1,如果每次扩容threshold都翻倍,
                 * 那负载因子是0.5了。
                 * 为什么只小于16呢?
                 * 我猜测是在每次扩容都计算threshold和用位运算翻倍之间做权衡
                 */
    			newThr = oldThr << 1; 
    	}
    	// 带参初始化会进入这里,主要是为了重新算threshold
    	else if (oldThr > 0) 
    		newCap = oldThr;
    	// 不带参初始化会进入这里
    	else {               
    		newCap = DEFAULT_INITIAL_CAPACITY;
    		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    	}
    	// 重新算threshold
    	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;
    	// 复制数据到新table中
    	if (oldTab != null) {
    		// 遍历Node
    		for (int j = 0; j < oldCap; ++j) {
    			Node<K,V> e;
    			if ((e = oldTab[j]) != null) {
    				oldTab[j] = null;
    				// 如果只有一个节点,则直接赋值
    				if (e.next == null)
    					newTab[e.hash & (newCap - 1)] = e;
    				// 如果是红黑树(较为复杂,不在这里说明)
    				else if (e instanceof TreeNode)
    					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    				else { 
    					// 之所以定义两个头两个尾对象,是由于链表中的元素的下标在扩容后,要么是原下标+oldCap,要么不变,下面会证实
    					Node<K,V> loHead = null, loTail = null;
    					Node<K,V> hiHead = null, hiTail = null;
    					Node<K,V> 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) {
    						// 尾部节点next设置为null,代码严谨
    						loTail.next = null;
    						newTab[j] = loHead;
    					}
    					// 新下标对应的链表
    					if (hiTail != null) {
    						hiTail.next = null;
    						newTab[j + oldCap] = hiHead;
    					}
    				}
    			}
    		}
    	}
    	return newTab;
    }
     
    ①带参构造方法
    public HashMap(int initialCapacity, float loadFactor) {
    	if (initialCapacity < 0)
    		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    	// 容量最大为MAXIMUM_CAPACITY(2^30)
    	if (initialCapacity > MAXIMUM_CAPACITY)
    		initialCapacity = MAXIMUM_CAPACITY;
    	if (loadFactor <= 0 || Float.isNaN(loadFactor))
    		throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    	this.loadFactor = loadFactor;
    	// threshold初始化为最接近initialCapacity的2的幂次方,并且大于或等于initialCapacity。但是在第一次put的时候,threshold会变成threshold * loadFactor
    	this.threshold = tableSizeFor(initialCapacity);
    }
     
    ②Map容量为什么不能为MAX_VALUE
    该为题可转为:为什么在Java1.8,每次扩容都为2的幂次方呢?
    // 计算下标,下面是map的put和get中都用到计算下标的
    (n - 1) & hash
     
    当容量为MAX_VALUE(2^31-1)时,转换成二进制
    	hash
    &
    	0111 1111 1111 1111 1111 1111 1111 1110
    -----------------------------------------------
            xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0
    从上面可看出最低位无论hash是任何值时,都为0,也就是下标只有2^30种可能,有2^30-1个下标没有被使用
    所以当容量为MAX_VALUE(2^31-1)时会造成一半的空间浪费,效率等同于MAXIMUM_CAPACITY(2^30)
     
    ③e.hash & oldCap
    该步骤是为了计算位置是否需要移动
    因为oldTab的元素下标是根据 hash(key) & (oldCap-1) 计算的,如果扩容后,计算下标是 hash(key) & (2*oldCap-1)
    换成二进制就比较清晰了
    

    其中看出低位和高位的亦或主要是是hash分布均匀。

    treeifyBin方法,应该可以解释为:把容器里的元素变成树结构。当HashMap的内部元素数组中某个位置上存在多个hash值相同的键值对,这些Node已经形成了一个链表,当该链表的长度大于等于9

        /**
     * tab:元素数组,
     * hash:hash值(要增加的键值对的key的hash值)
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
     
        int n, index; Node<K,V> e;
        /*
         * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制
         * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
         * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)
         * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
         */
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize(); // 扩容,可参见resize方法解析
     
        // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
        // 根据hash值和数组长度进行取模运算后,得到链表的首节点
        else if ((e = tab[index = (n - 1) & hash]) != null) { 
            TreeNode<K,V> hd = null, tl = null; // 定义首、尾节点
            do { 
                TreeNode<K,V> p = replacementTreeNode(e, null); // 将该节点转换为 树节点
                if (tl == null) // 如果尾节点为空,说明还没有根节点
                    hd = p; // 首节点(根节点)指向 当前节点
                else { // 尾节点不为空,以下两行是一个双向链表结构
                    p.prev = tl; // 当前树节点的 前一个节点指向 尾节点
                    tl.next = p; // 尾节点的 后一个节点指向 当前节点
                }
                tl = p; // 把当前节点设为尾节点
            } while ((e = e.next) != null); // 继续遍历链表
     
            // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
     
            // 把转换后的双向链表,替换原来位置上的单向链表
            if ((tab[index] = hd) != null)
                hd.treeify(tab);//此处单独解析
        }
    }
    

    后续部分继续补充。
    参考博客:https://blog.csdn.net/weixin_42340670/article/details/80503863
    https://blog.csdn.net/u010828343/article/details/80769385

  • 相关阅读:
    数据类型装换
    变量及数据类型
    27 网络通信协议 udp tcp
    26 socket简单操作
    26 socket简单操作
    14 内置函数 递归 二分法查找
    15 装饰器 开闭原则 代参装饰器 多个装饰器同一函数应用
    12 生成器和生成器函数以及各种推导式
    13 内置函数 匿名函数 eval,exec,compile
    10 函数进阶 动态传参 作用域和名称空间 函数的嵌套 全局变量
  • 原文地址:https://www.cnblogs.com/ontoweb-zp/p/10624028.html
Copyright © 2011-2022 走看看