HashMap深入讲解

打印 上一主题 下一主题

主题 663|帖子 663|积分 1989

HashMap是Java中最常用的集合类框架,也是Java语言中非常典型的数据结构,
HashSetHashMap者在Java里有着雷同的实现,前者仅仅是对后者做了一层包装,也就是说HashSet内里有一个HashMap(适配器模式)。因此了解HashMap源码也就了解HashSet了
介绍


  • Key的存储方式是基于哈希表的
  • HashMap是 Map 接口 使用频率最高的实现类。
  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  • 全部的key构成的集合是无序的、唯一不可重复的。所以,key所在的类要重写:equals()和hashCode()
  • 全部的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  • 一个key-value构成一个entry
  • 全部的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
  • HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true
底层原理介绍

底层数据结构和初始属性
  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //  HashMap的默认初始容量,16
  2. static final int MAXIMUM_CAPACITY = 1 << 30;//HashMap的最大支持容量,2^30
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f;//HashMap的默认加载因子
  4. static final int TREEIFY_THRESHOLD = 8;//Bucket中链表长度大于该默认值,转化为红黑树
  5. static final int UNTREEIFY_THRESHOLD = 6;//Bucket中红黑树存储的Node小于该默认值,转化为链表
  6. /**
  7. * 桶中的Node被树化时最小的hash表容量。
  8. *(当桶中Node的数量大到需要变红黑树时,
  9. * 若hash表容量小于MIN_TREEIFY_CAPACITY时,
  10. * 此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
  11. */
  12. static final int MIN_TREEIFY_CAPACITY = 64;
  13. //存储元素的数组,总是2的n次幂
  14. //通过数组存储,数组的元素是具体的Node<K,V>,这个Node有可能组成红黑树,可能是链表
  15. transient Node<K,V>[] table;
  16. //存储具体元素的集
  17. transient Set<Map.Entry<K,V>> entrySet;
  18. //HashMap中存储的键值对的数量
  19. transient int size;
  20. //扩容的临界值,=容量*加载因子
  21. int threshold;
  22. //The load factor for the hash table.
  23. final float loadFactor;
复制代码
这里分为了三步:

  • h=key.hashCode() //第一步 取hashCode值
  • h^(h>>>16) //第二步 高位到场运算,减少辩论,hash计算到这里
  • return h&(length-1); //第三步 取模运算,计算数据在桶中的位置,这里看后面的源码
第3步(n-1)&hash原理:

  • 实际上(n-1) & hash等于 hash%n都可以得到元素在桶中的位置,但是(n-1)&hash操纵更快。
  • 取余操纵如果除数是 2 的整数次幂可以优化为移位操纵。这也是为什么扩容时必须是必须是2的n次方
位运算(&)效率要比代替取模运算(%)高很多,主要缘故原由是位运算直接对内存数据进行操纵,不需要转成十进制,因此处理速度非常快。
而计算hash是通过同时使用hashCode()的高16位异和低16位实现的(h >>> 16):这么做可以在数组比力小的时候,也能保证考虑到高低位都到场到Hash的计算中,可以减少辩论,同时不会有太大的开销。
hash值实在是一个int类型,二进制位为32位,而HashMap的table数组初始化size为16,取余操纵为hashCode & 15 ==> hashCode & 1111 。这将会存在一个巨大的问题,1111只会与hashCode的低四位进行与操纵,也就是hashCode的高位实在并没有到场运算,会导很多hash值不同而高位有区别的数,最后算出来的索引都是一样的。 举个例子,假设hashCode为1111110001,那么1111110001 & 1111 = 0001,如果有些key的hash值低位与前者雷同,但高位发生了变化,如1011110001 & 1111 = 0001,1001110001 & 1111 = 0001,显然在高位发生变化后,最后算出来的索引照旧一样,如许就会导致很多数据都被放到一个数组内里了,造成性能退化。 为了制止这种情况,HashMap将高16位与低16位进行异或,如许可以保证高位的数据也到场到与运算中来,以增大索引的散列程度,让数据分布得更为均匀(个人认为是为了分布均匀)
put流程如下:
  1. //空参构造,初始化加载因子
  2. public HashMap() {
  3.     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  4. }
  5. //有参构造,可以初始化初始容量大小和加载因子
  6. public HashMap(int initialCapacity, float loadFactor) {
  7.     if (initialCapacity < 0)
  8.         throw new IllegalArgumentException("Illegal initial capacity: " +
  9.                                           initialCapacity);
  10.     if (initialCapacity > MAXIMUM_CAPACITY)
  11.         initialCapacity = MAXIMUM_CAPACITY;
  12.     if (loadFactor <= 0 || Float.isNaN(loadFactor))
  13.         throw new IllegalArgumentException("Illegal load factor: " +
  14.                                                loadFactor);
  15.     this.loadFactor = loadFactor;
  16.     this.threshold = tableSizeFor(initialCapacity);//扩容的临界值,= 容量*加载因子
  17. }
复制代码
  1. public V put(K key, V value) {
  2.     return putVal(hash(key), key, value, false, true);
  3. }
复制代码
总结put方法流程:

  • 如果table没有初始化就先进行初始化过程
  • 使用hash算法计算key的索引判断索引处有没有存在元素,没有就直接插入
  • 如果索引处存在元素,则遍历插入,有两种情况,

    • 一种是链表形式就直接遍历到尾端插入,
    • 一种是红黑树就按照红黑树结构

  • 插入链表的数量大于阈值8,且数组大小已经大等于64,就要转换成红黑树的结构
  • 添加成功后会查抄是否需要扩容
数组扩容
  1. static final int hash(Object key) {
  2.     int h;
  3.     //为什么要右移16位? 默认长度为2^5=16,与hash值&操作,容易获得相同的值。
  4.     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  5. }
复制代码
显然,HashMap的扩容机制,就是当达到扩容条件时会进行扩容。扩容条件就是当HashMap中的元素个数凌驾临界值时就会自动扩容(threshold = loadFactor * capacity)。如果是初始化扩容,只执行resize的前半部分代码,但如果是随着元素的增长而扩容,HashMap需要重新计算oldTab中每个值的位置,即重修hash表,随着元素的不断增长,HashMap会发生多次扩容,如许就会非常影响性能。所以一般发起创建HashMap的时候指定初始化容量
但是当使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用传进来的initialCapacity直接作为初始容量。JDK会默认计算一个相对合理的值当做初始容量。所谓合理值,实在是找到第一个比用户传入的值大的2的幂。也就是说,当new HashMap(7)创建HashMap的时候,JDK会通过计算,创建一个容量为8的Map;当new HashMap(9)创建HashMap的时候,JDK会通过计算,创建一个容量为16的Map。当然了,当创建一个HashMap时,表的大小并不会立刻分配,而是在第一次put元素时进行分配,并且分配的大小会是大于或等于初始容量的最小的2的幂。
一般来说,initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增长而被迫不断扩容,resize() 方法统共会调用 8 次,反复重修哈希表和数据迁徙。当放置的集合元素个数达千万级时会影响程序性能。
  1. // 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
  2. // 第五个参数 evict 我们这里不关心
  3. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  4.                boolean evict) {
  5.     Node<K,V>[] tab; Node<K,V> p; int n, i;
  6.     if ((tab = table) == null || (n = tab.length) == 0)//初始判断,初始数组为空时
  7.         // resize()初始数组,需要进行扩容操作
  8.         n = (tab = resize()).length;
  9.     //这里就是上面的第三步,根据key的hash值找到数据在table中的位置
  10.     if ((p = tab[i = (n - 1) & hash]) == null)
  11.         //通过hash找到的数组下标,里面没有内容就直接赋值
  12.         tab[i] = newNode(hash, key, value, null);
  13.     else {//如果里面已经有内容了
  14.         Node<K,V> e; K k;
  15.         if (p.hash == hash &&
  16.             //hash相同,key也相同,那就直接修改value值
  17.             ((k = p.key) == key || (key != null && key.equals(k))))
  18.             e = p;
  19.         else if (p instanceof TreeNode)
  20.             //key不相同,且节点为红黑树,那就把节点放到红黑树里
  21.             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  22.         else {
  23.         //表示节点是链表
  24.             for (int binCount = 0; ; ++binCount) {
  25.                 if ((e = p.next) == null) {
  26.                     //添加到链表尾部
  27.                     p.next = newNode(hash, key, value, null);
  28.                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  29.                  //如果满足链表转红黑树的条件,则转红黑树
  30.                         treeifyBin(tab, hash);
  31.                     break;
  32.                 }
  33.                 if (e.hash == hash &&
  34.                     ((k = e.key) == key || (key != null && key.equals(k))))
  35.                     break;
  36.                 p = e;
  37.             }
  38.         }
  39.         //传入的K元素已经存在,直接覆盖value
  40.         if (e != null) { // existing mapping for key
  41.             V oldValue = e.value;
  42.             if (!onlyIfAbsent || oldValue == null)
  43.                 e.value = value;
  44.             afterNodeAccess(e);
  45.             return oldValue;
  46.         }
  47.     }
  48.     ++modCount;
  49.     if (++size > threshold)//检查元素个数是否大于阈值,大于就扩容
  50.         resize();
  51.     afterNodeInsertion(evict);
  52.     return null;
  53. }
复制代码
  1. final void treeifyBin(Node<K,V>[] tab, int hash) {
  2.     int n, index; Node<K,V> e;
  3.     //检查是否满足转换成红黑树的条件,如果数组大小还小于64,则先扩容
  4.     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  5.         resize();
  6.     else if ((e = tab[index = (n - 1) & hash]) != null) {
  7.         TreeNode<K,V> hd = null, tl = null;
  8.      do {
  9.            TreeNode<K,V> p = replacementTreeNode(e, null);
  10.            if (tl == null)
  11.                 hd = p;
  12.             else {
  13.                 p.prev = tl;
  14.                 tl.next = p;
  15.             }
  16.             tl = p;
  17.        } while ((e = e.next) != null);
  18.        if ((tab[index] = hd) != null)
  19.            hd.treeify(tab);
  20.     }
  21. }
复制代码
总结get方法:

  • 起首通过hash()函数得到对应数组下标,然后依次判断。
  • 判断第一个元素与key是否匹配,如果匹配就返回参数值;
  • 判断链表是否红黑树,如果是红黑树,就进入红黑树方法获取参数值;
  • 如果不是红黑树结构,直接循环遍历链表判断,直到获取参数为止;
remove方法

jdk1.8的删除逻辑实现比力复杂,删除时有红黑树节点删除和调解:

  • 默认判断链表第一个元素是否是要删除的元素;
  • 如果第一个不是,就继续判断当前辩论链表是否是红黑树,如果是,就进入红黑树内里去找;
  • 如果当前辩论链表不是红黑树,就直接在链表中循环判断,直到找到为止;
  • 将找到的节点,删撤除,如果是红黑树结构,会进行颜色转换、左旋、右旋调解,直到满足红黑树特性为止;
HashSet


  • Set 不能存放重复元素,无序的,允许一个null(基于HashMap 实现,HashMap的key可以为null);
  • Set 基于 Map 实现,Set 里的元素值就是 Map的键值。
HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。
底层源码
  1. //table数组的扩容操作
  2. final Node<K,V>[] resize() {
  3.     //引用扩容前的node数组
  4.     Node<K,V>[] oldTab = table;
  5.     //旧的容量
  6.     int oldCap = (oldTab == null) ? 0 : oldTab.length;
  7.     //旧的阈值
  8.     int oldThr = threshold;
  9.     //新的容量、阈值初始化为0
  10.     int newCap, newThr = 0;
  11.    
  12.     //计算新容量
  13.     if (oldCap > 0) {
  14.         //如果旧容量已经超过最大容量,让阈值也等于最大容量,以后不再扩容
  15.         if (oldCap >= MAXIMUM_CAPACITY) {
  16.             threshold = Integer.MAX_VALUE;
  17.             return oldTab;
  18.         }
  19.         //没超过最大值,就令newcap为原来容量的两倍
  20.         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  21.                  oldCap >= DEFAULT_INITIAL_CAPACITY)
  22.             //如果旧容量翻倍没有超过最大值,且旧容量不小于初始化容量16,则翻倍
  23.             newThr = oldThr << 1; // double threshold
  24.     }
  25.     else if (oldThr > 0) // initial capacity was placed in threshold
  26.         //旧容量oldCap = 0时,但是旧的阈值大于0,令初始化容量设置为阈值
  27.         newCap = oldThr;
  28.     else {               // zero initial threshold signifies using defaults
  29.         //两个值都为0的时候使用默认值初始化
  30.         newCap = DEFAULT_INITIAL_CAPACITY;
  31.         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  32.     }
  33.    
  34.    
  35.     if (newThr == 0) {
  36.         //计算新阈值,如果新容量或新阈值大于等于最大容量,则直接使用最大值作为阈值,不再扩容
  37.         float ft = (float)newCap * loadFactor;
  38.         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  39.                   (int)ft : Integer.MAX_VALUE);
  40.     }
  41.     //设置新阈值
  42.     threshold = newThr;
  43.    
  44.     //创建新的数组,并引用
  45.     @SuppressWarnings({"rawtypes","unchecked"})
  46.         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  47.     table = newTab;
  48.    
  49.     //如果老的数组有数据,也就是是扩容而不是初始化,才执行下面的代码,否则初始化的到这里就可以结束了
  50.     if (oldTab != null) {
  51.         //轮询老数组所有数据
  52.         for (int j = 0; j < oldCap; ++j) {
  53.             //以一个新的节点引用当前节点,然后释放原来的节点的引用
  54.             Node<K,V> e;
  55.             if ((e = oldTab[j]) != null) {//如果这个桶,不为空,说明桶中有数据
  56.                 oldTab[j] = null;
  57.                 //如果e没有next节点,证明这个节点上没有hash冲突,则直接把e的引用给到新的数组位置上
  58.                 if (e.next == null)
  59.                     //确定元素在新的数组里的位置
  60.                     newTab[e.hash & (newCap - 1)] = e;
  61.                 //如果是红黑树,则进行分裂
  62.                 else if (e instanceof TreeNode)
  63.                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  64.                 //说明是链表
  65.                 else { // preserve order
  66.                     Node<K,V> loHead = null, loTail = null;
  67.                     Node<K,V> hiHead = null, hiTail = null;
  68.                     Node<K,V> next;
  69.                     //从这条链表上第一个元素开始轮询,如果当前元素新增的bit是0,则放在当前这条链表上
  70.                     //如果是1,则放在"j+oldcap"这个位置上,生成“低位”和“高位”两个链表
  71.                     do {
  72.                         next = e.next;
  73.                         if ((e.hash & oldCap) == 0) {
  74.                             if (loTail == null)
  75.                                 loHead = e;
  76.                             else
  77.                                 //元素是不断的加到尾部的
  78.                                 loTail.next = e;
  79.                             //新增的元素永远是尾元素
  80.                             loTail = e;
  81.                         }
  82.                         else {
  83.                             //高位的链表与低位的链表处理逻辑一样,不断的把元素加到链表尾部
  84.                             if (hiTail == null)
  85.                                 hiHead = e;
  86.                             else
  87.                                 hiTail.next = e;
  88.                             hiTail = e;
  89.                         }
  90.                     } while ((e = next) != null);
  91.                     //低位链表放到j这个索引的位置上
  92.                     if (loTail != null) {
  93.                         loTail.next = null;
  94.                         newTab[j] = loHead;
  95.                     }
  96.                     //高位链表放到(j+oldCap)这个索引的位置上
  97.                     if (hiTail != null) {
  98.                         hiTail.next = null;
  99.                         newTab[j + oldCap] = hiHead;
  100.                     }
  101.                 }
  102.             }
  103.         }
  104.     }
  105.     return newTab;
  106. }
复制代码
关于作者

来自一线程序员Seven的探索与实践,连续学习迭代中~
本文已收录于我的个人博客:https://www.seven97.top
公众号:seven97,欢迎关注~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

铁佛

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表