jdk1.8 HashMap-源码学习

记一次1.8的hashmap源码学习

jdk1.8 HashMap

  • hashmap初始化后设定初始阈值,并不初始化。threshold 计算2的n次幂。

    比如构造3的hashmap,最终初始化容量为4,threshold为3。没有设置初始构造容量,那么在初始化的时候默认为16。

  1. 构造
 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);
    }

tableSizeFor

   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;
    }
  • 第一次putval时候,如果表为空的话resize方法()初始化initCap = threshold的hash数组,threshold=loadFactor*initCap; 然后oldhash表不为空就, rehash。
  1. 初次初始化的时候。
Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
newCap = oldThr;

 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

 return newTab;

//oldTab不为空  rehash 元素移动

resize方法,其中包含rehash,将oldTab数据转移到新的hash表。

  • HashMap的初始化是在第一次put的时候完成的。

rehash是在需要扩容时机才执行的,一次rehash完。

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果oldCap不为空的话,就是hash桶数组不为空
        if (oldCap > 0) {
            //如果大于最大容量了,就赋值为整数最大的阀值 threshold = Integer.MAX_VALUE;
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
                  newThr = oldThr << 1; // double threshold
            }
   				// 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化 成最小2的n次幂
   				// 直接将该值赋给新的容量
           else if (oldThr > 0) // initial capacity was placed in threshold
           			 newCap = oldThr;
   				// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
       		 else {               // zero initial threshold signifies using defaults
           			 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;
        @SuppressWarnings({"rawtypes","unchecked"})
        //初始化新的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
   		//如果old表有数据 需要数据迁移
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
               
                if ((e = oldTab[j]) != null) {
                  // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无 法被GC回收
                    oldTab[j] = null;
                    if (e.next == null)
                        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);
                   // 链表头节点
                  else { // preserve order
                    
                    // loHead,loTail 代表扩容后不用变换下标,见注1
                        Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标,见注1
                        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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                          //j原有的hash索引,oldcap+j = 新的hash索引  
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

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;
        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 {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          //红黑树
            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) {
                       //链表尾部插入 node
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                           // 判断是否需要转换为红黑树 8
                            treeifyBin(tab, hash);
                        break;
                    }
                  // 判断hashcode、key是否相等,相等覆盖返回该node 即后面的e
                  //  ((k = e.key) == key 普通类类型 判断直接
                  // (key != null && key.equals(k)) 则用于判断复杂类类型
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
          //判断是否有返回的Node 即有key相等情况 需要覆盖value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
              //访问后的回调方法 子类实现
                afterNodeAccess(e);
              // 返回old value
                return oldValue;
            }
        }
  // put完 modCount++ 保证线程安全的 fast-fail机制
        ++modCount;
        if (++size > threshold)
            resize();
				// 调用插入完成的后置方法 子类实现
        afterNodeInsertion(evict);
        return null;
    }

当我们put的时候,首先计算 key 的 hash 值,这里调用了 hash 方法, hash 方法实际是让key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

putval大致流程:

图片
  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;

  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

hash函数

就算hashcode分布的再松散,要是只取最后几位的话,碰撞也会很严重。进一步降低hash碰撞的概率,使得数据分布更平均,这样的操作被称为扰动

static final int hash(Object key) { 
  // 与自己 右移16位进行异或运算(高低位异或)
  int h; 
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
}

常见问题

HashMap 为什么使用链表还要红黑树
  • 链表查询效率低,插入删除效率高
  • 红黑树查询效率高,插入删除效率低
HashMap 为什么使用红黑树么不使用平衡二叉树

红黑树的平衡调整开销相比于AVL的多次旋转要小,效率更高。

为什么要设计大于阈值8转红黑树,小于6才转链表?

主要是7作为缓冲,如果直接设计为大于8红黑树,小于8链表的话,如果插入较为频繁的话,就有可能造成频繁的链表和红黑树之间的切换,开销更大。

为什么选择0.75作为默认的负载因子

过大造成 hash冲突的可能性更高,put效率降低;如果过小的话,那就造成过大的空间浪费。而取0.75是综合考虑时间和空间的结果。

22.jdk7的HashMap为什么采用头插法?

HashMap的开发者认为后面被插入的对象,被查找的可能性更大,而链表查找元素从头遍历,所以采用头插法往往能够提升查找效率。

23.jdk8相比之前的版本,解决了HashMap哪些线程安全问题?

jdk8的HashMap在处理hash冲突时,采用尾插法代替头插法,扩容时避免了翻转链表的发生,也就是解决了循环链表的问题。

25.链表转化为红黑树的阈值为啥是8呢?

如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为8的时候,概率概率仅为十万分之6,这么小的概率,HashMap的红黑树转换几乎不会发生。

  • 大于8的链表查询速度很慢,而在实际使用中,hashCode设计的好,很小的概率会发生转换为红黑树。
String最适合作为Key

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

在java的对象头部有一个8byte的markword字段会在无锁状态下缓存该对象的一个hashcode信息。

img

30.除了链表尾插和红黑树,HashMap在jdk1.8和jdk1.7还有哪些差异?

1)JDK1.7是先扩容后插入,jdk1.8是先插入后扩容。

2)JDK1.7扩容需要重新hash来定位每个元素在桶中的位置,jdk1.8不需要

你说说红黑树的特点?

1)每个节点非红即黑

2)根节点总是黑色的

3)如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

4)每个叶子节点都是黑色的空节点(NIL节点)

5)从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

linkedHashMap

LinkedHashMap 需要重写实现的方法

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bY9rUbOX-1657099033314)(/Users/skybox/Library/Application%20Support/typora-user-images/image-20220705115130424.png)]

阿里巴巴规范

首先先放出结论,根据阿里巴巴java开发规范中,第一章第五节第9点;

【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化, 正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader
factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。

反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。

所以实际开发过程中能确定hashmap的容量就尽量按照以上公式确定初始容量,否则默认16。

也就是公式:
n ----- 需要存储的元素个数
c ----- 建议设置容量大小
c = (n * 4 / 3) + 1
如果我们需要存3个,按这个公式应该设置为5,而不是4,其实它的容量也不是5,而是8。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值