JDK集合之Map

HashMap是Java集合中的键值对存储结构,其在JDK1.8后使用数组+链表+红黑树实现。HashMap非线程安全,若需线程安全可选ConcurrentHashMap。本文详细介绍了HashMap的定义、特点、扩容机制,包括正常扩容和哈希冲突时的扩容,并分析了put方法和get方法的调用过程。此外,还探讨了TreeMap的使用。
摘要由CSDN通过智能技术生成

一、定义

二、实现

1.1、HashMap

1、定义

HashMap是通过键值形式存储数据的一种集合。在jdk1.7版本,HashMap的底层数据结构基于数组+链表;在jdk1.8版本,HashMap的底层数据结构基于数组+链表+红黑树。HashMap的最大容量为2的30次方,即十亿加

HashMap是线程不安全的集合,若要保证线程安全,需要选择Hashtable、CocurrentHashMap这些集合类,或者使用Collections.synchronizedMap()构造一个线程安全的Map。 。

注意:

  1. HashMap选择红黑树而不是AVL数的原因
    • 红黑树插入和删除性能相对较好:红黑树在插入删除方面,相对于AVL树,它的平衡调整次数较少,性能更优,但是查询方面,由于AVL是更加严格的平衡树,所以查询比较次数更少,查询性能更好;
    • 红黑树实现更加简单:相对于AVL数,红黑树在实现方面不需要维护节点的平衡因子,代码实现更加简洁;
    • 红黑树占用空间更少:红黑树不需要维护节点的平衡因子,占用的空间更少。
  2. HashMap不直接使用红黑树的原因

        在未树化阶段,链表和红黑树查询性能都差不多,但是树节点占用空间被普通节点多,所以前期使用红黑树空间利用率较低 

2、特性

3、原理

(1)扩容机制
A、正常扩容

HashMap调用put方法时会判断是否进行扩容,判断依据是:哈希表大小是否等于0或者大于扩容阈值扩容过程是:先判断哈希表容量是否等于0,若等于0,则直接扩容到16,扩容阈值为12若哈希表容量大于0,根据扩容阈值*2倍进行扩容。其中阈值的计算是容量乘以负载因子(默认0.75)

B、哈希冲突时的扩容(哈希冲突的过程)

哈希冲突即多个对象的key的hashCode相同,定位到哈希表相同的位置。 当出现哈希冲突的时候,会先判断当前节点是链表节点还是树节点,若是链表节点则会先把该元素放在链表中,然后判断当前链表长度是否大于等于8,是的话会进行树化,在树化过程中,会再判断当前哈希表容量是否大于等于64是的话,则会把链表转为红黑树否则,则会调用扩容机制。若是树节点则会通过红黑树的方式加入。

注意:

  • 若在数据量非常大的情况下,扩容是会带来性能损失的。
  • 链表长度大于等于8会树化的原因:

        根据概率统计,长度为8的概率小于千万分之一,所以链表长度很难达到8,树节点也很少使用。在这种情况下,链表性能已经非常差,所以在需要把链表转为红黑树,保证查询的性能。

  • HashMap的负载因子设置为多少合适?

        HashMap的负载因子默认是0.75,用于和哈希表容量乘积表示扩容阈值,只有大于该阈值才会进行扩容,而负载因子是可以根据应用场景和性能需求进行调整。若更注重性能,则可以降低负载因子,这样哈希表的扩容阈值变小,扩容频率变高,会消耗更多的内存,但是哈希冲突的几率降低,提高了查询速度;若更注重内存效率,可以提高负载因子,这样哈希表扩容阈值变大,扩容频率降低,内存利用率变高,但是哈希冲突的几率也会更高,查询速度也会越来越低;

(2)put方法
A、介绍

HashMap利用put方法进行添加元素,而put方法的核心方法是putVal方法。

B、put方法调用过程
  1. put(K key, V value)
    1. putVal(hash(key), key, value, false, true)
    2. hash(Object key)
      1. (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
    3. putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)(onlyIfAbsent表示是否不可以覆盖key,true表示不可以覆盖相同的key,默认是false)
      1. Node<K,V>[] tab; Node<K,V> p; int n, i
      2. if ((tab = table) == null || (n = tab.length) == 0)(判断哈希表是否需要扩容)
        1. n = (tab = resize()).length
          1. resize()
            1. Node<K,V>[] oldTab = table;(记录旧的哈希表)
            2. int oldCap = (oldTab == null) ? 0 : oldTab.length;(记录旧的哈希表的容量)
            3. int oldThr = threshold;(记录旧的哈希表的阈值)
            4. int newCap, newThr = 0;
            5. if (oldCap > 0)(计算需要扩容的大小)
              1. if (oldCap >= MAXIMUM_CAPACITY)(判断旧哈希表的容量是否大于等于最大容量
                1. threshold = Integer.MAX_VALUE(是,则阈值修改为最大容量)
                2.  return oldTab返回旧的哈希表
              2. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)(判断旧容量的2倍是否小于最大容量且旧容量大于默认初始容量16
                1.  newThr = oldThr << 1; (是,则扩容容量为旧容量的2倍(在上面判断过程赋予给了newCap)且更新阈值为旧阈值的2倍
              3. else if (oldThr > 0)(这里可能是旧容量的2倍小于默认初始容量16或者旧容量的2倍大于等于最大容量时,需要继续判断旧阈值是否大于0
                1. newCap = oldThr(是,则更新新容量为旧阈值
              4. else(旧阈值为0(说明未扩容过))
                1.  newCap = DEFAULT_INITIAL_CAPACITY设置新容量为默认初始容量16
                2. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)(设置新阈值为新容量*默认负载因子)
              5. if (newThr == 0)(计算初始容量新的阈值)
                1. float ft = (float)newCap * loadFactor
                2. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                                        (int)ft : Integer.MAX_VALUE)
              6.  threshold = newThr(更新阈值)
              7. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap](创建新的哈希表
              8. table = newTab
              9. if (oldTab != null)(判断旧的哈希表是否为空)
                1. for (int j = 0; j < oldCap; ++j)(不为空,把旧哈希表的每个元素设置到新哈希表
                  1.  Node<K,V> e
                  2.  if ((e = oldTab[j]) != null)
                    1. oldTab[j] = null;
                    2. if (e.next == null)(普通元素
                      1. newTab[e.hash & (newCap - 1)] = e(普通元素:扩容后的下标只有两种情况:和旧下标相等或者旧下标+旧容量)
                    3. else if (e instanceof TreeNode)(树节点
                      1. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap)(将红黑树的节点进行拆分,将树中的每个节点都存入新位置,同时判断是否需要进行树转链表)
                    4. else(遍历链表,将链表分为两部分,一部分(loHead)是索引不变,一部分(hiHead)的新索引是 oldIndex+oldCapacity,然后将链表放入对应的数组中)
                      1. Node<K,V> loHead = null, loTail = null;
                      2. Node<K,V> hiHead = null, hiTail = null;
                      3. Node<K,V> next;
                      4. do{
                        1. next = e.next;
                        2. if ((e.hash & oldCap) == 0)
                          1. if (loTail == null)
                            1. loHead = e
                          2.  else
                            1. loTail.next = e;
                          3. loTail = e;
                        3. else
                          1. if (hiTail == null)
                            1. hiHead = e
                          2. else
                            1. hiTail.next = e
                          3. hiTail = e
                      5. }while ((e = next) != null)
                      6. if (loTail != null)
                        1. loTail.next = null;
                        2. newTab[j] = loHead;
                      7. if (hiTail != null) 
                        1. hiTail.next = null;
                        2. newTab[j + oldCap] = hiHead;
              10. return newTab
      3. if ((p = tab[i = (n - 1) & hash]) == null)(判断哈希表对应下标的位置是否为空)
        1. tab[i] = newNode(hash, key, value, null);(为空,则直接插入该元素
      4. else找到哈希表中相同key的节点,并更新到e;或者不存在相同key的节点,则需要创建一个新节点,e则为null
        1. Node<K,V> e; K k;
        2. if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))(判断已存在节点的hash和key是否和新节点相同
          1. e = p;(是,更新e值为这个相同节点
        3. else if (p instanceof TreeNode)(判断已存在节点是否为树节点
          1. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        4. else (已存在节点为链表节点
          1. for (int binCount = 0; ; ++binCount)
            1. if ((e = p.next) == null) 
              1. p.next = newNode(hash, key, value, null)
              2. if (binCount >= TREEIFY_THRESHOLD - 1)(判断链表是否需要树化
                1. treeifyBin(tab, hash)
              3. break
            2. if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
              1. break
            3. p = e
        5. if (e != null)哈希表中存在该key的元素,则要判断是否需要覆盖
          1. V oldValue = e.value;(记录旧值)
          2. if (!onlyIfAbsent || oldValue == null)(判断onlyIfAbsent为false,即允许覆盖;或者判断旧值是否为空)
            1. e.value = value(允许覆盖,更新e的值
          3. afterNodeAccess(e)
          4. return oldValue(成功添加,返回旧值,若是已经存在的元素,则不需要扩容
      5. ++modCount
      6. if (++size > threshold)(判断哈希表当前大小是否大于阈值
        1. resize()(是,则进行扩容)
      7. afterNodeInsertion(evict)
      8. return null(返回null)
(3)put方法大体流程
  1. 计算key的哈希值,若key为null,则哈希值为0;【哈希值作为参数传入putVal方法】
  2. 判断哈希表是否为空或者大小为0,是则进行扩容
  3. 根据哈希值定位到哈希表对应的位置;
  4. 判断该位置是否为空
    1. 为空,直接new一个节点插入到该位置
    2. 不为空(找到需要替换的节点e(若不存在则需要创建e节点))
      1. 判断该位置是否已经存在相同的键的元素,是则更新该相同节点到e;
      2. 判断该位置的节点是否为树节点
        1. 是,则创建一个新节点赋给e,并插入到该树
        2. 否则,创建一个新节点赋给e,利用尾插法插入到链表
          1. 继续判断链表长度是否大于等于8且哈希表容量大于等于64,是则把链表转为红黑树,否则进行扩容;
      3. 根据传入的onlyIfAbsent值判断是否更新节点e的value,若onlyIfAbsent为false则更新节点e的value为新值,并返回旧值;为true则返回null
  5. 插入成功,增加modCount和size,若size超过了扩容阈值,则需要进行扩容
(3)get方法
A、介绍

HashMap利用get方法获取对应key的值,get方法的核心方法是getNode方法。

B、方法调用过程
  1. get(Object key)
    1. (e = getNode(hash(key), key)) == null ? null : e.value
      1. hash(Object key)
        1. (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
      2. getNode(int hash, Object key)
        1. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        2. if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) (判断哈希表对应下标的节点是否存在
          1. if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))(判断哈希表对应下标节点的hash和key是否相同
            1. return first
          2. if ((e = first.next) != null)(first节点的hash和key和目标节点不相同,则需要继续遍历链表或树)
            1. if (first instanceof TreeNode)(判断first节点是否为树节点
              1. return ((TreeNode<K,V>)first).getTreeNode(hash, key)
            2. do{(否则,first节点为链表节点
              1. if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
              2. return e
            3. } while ((e = e.next) != null)(向后遍历链表
      3. return null
C、大体流程
  1. 计算key的哈希值,若key为null,则哈希值为0。【哈希值作为参数传入getNode】
  2. 判断哈希表是否为空或者大于为0,是的话,直接返回空,不是则继续后面的流程
  3. 根据哈希值定位到哈希表对应的位置
  4. 判断该位置是否为null,是则直接返回空,否则继续执行后面的流程
  5. 判断该位置的哈希值和key是否和目标节点一致,是则直接返回该节点,否则继续执行后面的流程
  6. 判断该位置的节点的下一节点是否为null,是则直接返回空,否则继续执行后面的流程
  7. 判断该位置的节点是否为树节点,是则通过红黑树方式获取节点信息,否则循环判断链表的每个节点是否和目标节点的哈希值和key一致,当遍历到的节点为空或者与目标节点一致则跳出循环。

1.2、TreeMap

三、线程安全的Map

1、线程安全的HashMap

(1)ConcurrentHashMap

ConcurrentHashMap是线程安全版的HashMap,它的操作和HashMap相似,只是在一些对哈希表修改的地方的处理方式有点不同。HashMap直接赋予新元素给哈希表目标下标为null的位置,而ConcurrentHashMap通过CAS方式修改哈希表目标下标的值从null到新元素。HashMap在发生哈希冲突时直接在链表或者红黑树上插入元素,而ConcurrentHashMap是通过synchronized关键字锁住发生冲突的节点再继续再链表或者红黑树上插入元素。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值