HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的put()底层实现

JDK1.7 VS JDK1.8 比较

1.不同点

不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()在resize()中会初始化
hash计算方式扰动处理= 9次扰动= 4次位运算+ 5次异或运算扰动处理= 2次扰动= 1次位运算 + 1次异或运算
数据存储方式无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8,树化并存放在红黑树
插入数据方式头插法(先将原位置的数据移到后1 位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后原有位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->>(h&lengt h-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

2.JDK1.8实现的优点

  1. resize 扩容优化
  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

put()流程及理解

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 //真正实现put()方法
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		// 步骤①:table未初始化或者长度为0,进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            // 步骤②:计算index,并对null做处理
		   // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,封装结点放入桶中(此时,这
个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //对应的桶中已经存在节点
        else {
            Node<K,V> e; K k;
            // 步骤③:节点key存在,直接覆盖value
		    // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等,value覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //存储头节点,后续用来判断是否为类型为TreeNode
                e = p;
            // 步骤④:判断该链为红黑树
			// 如果当前头节点类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null    
            else if (p instanceof TreeNode)
            	//插入红黑树
                e = ((TreeNode<K,V>)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);
                        //判断该链表的长度是否达到转化红黑树的临界值,默认8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	//链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                     // 判断链表中结点的key值与插入的元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        break;
                    // 用于遍历桶中的链表及后续oldVal覆盖,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的val
ue这个值
            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;
    }

步骤:
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样(比较hash值和key值),如果相同直接覆盖value,否则转向
④,这里的相同指的是key.hashCode()以及key.equals();
④.判断table[i] 是否为TreeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值
对,否则转向⑤;
⑤.遍历table[i]这个链表,判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了 大容量threshold,如果超过,进行扩容。
4.put()图解
HashMap的put()流程

HashMap的扩容操作

①.在jdk1.8中,resize()是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
②.每次扩展的时候,都是扩展2倍 (n - 1) & hash;
③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

  final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab指向原来的hash桶数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果原有的hash桶数组不为空
            if (oldCap >= MAXIMUM_CAPACITY) {
            //如果大于最大容量了(达到整数的最大值),就赋值为整数最大的阀值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //阈值变为原来的2倍
                newThr = oldThr << 1; // double threshold
        }
        // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
		// 直接将该值赋给新的容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 新的threshold = 新的cap * 0.75
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 计算出新的数组长度后赋给当前成员变量table
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;//原数组指向新的扩容后的数组
        // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
        if (oldTab != null) {
        // 遍历新数组的所有桶下标
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                    oldTab[j] = null;
                    // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                    if (e.next == null)
                    // 用同样的hash映射算法把该元素加入新的数组
                        newTab[e.hash & (newCap - 1)] = e;
                      // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排  
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                     // e是链表的头并且e.next!=null,那么处理链表中元素重排
                    else { // preserve order
                    	// loHead,loTail 代表扩容后不用变换下标,
                        Node<K,V> loHead = null, loTail = null;
                        // hiHead,hiTail 代表扩容后变换下标,
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                 // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
								 // 代表下标保持不变的链表的头元素
                                    loHead = e;
                                else
                                	// loTail.next指向当前e
                                    loTail.next = e;
                                    8 // loTail指向当前的元素e
								 // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
 								// 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
								// 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                                loTail = e;
                            }
                            else {
                            // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                         // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值