【collection】1.java容器之HashMap&LinkedHashMap&Hashtable

打印 上一主题 下一主题

主题 1665|帖子 1665|积分 4995

Map源码剖析

HashMap&LinkedHashMap&Hashtable

hashMap默认的阈值是0.75
HashMap put操作

put操作涉及3种结构,普通node节点,链表节点,红黑树节点,针对第三种,红黑树节点,我们后续单独去学习,这里不多做扩散
  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2.                            boolean evict) {
  3.         Node<K,V>[] tab; Node<K,V> p; int n, i;
  4.         if ((tab = table) == null || (n = tab.length) == 0) {
  5.                 // 初始化哈希数组,或者对哈希数组扩容,返回新的哈希数组
  6.                 tab = resize();
  7.                 n = tab.length;
  8.         }
  9.         // 相当于取余
  10.         i = (n - 1) & hash;
  11.         p = tab[i];
  12.         if (p == null) {
  13.                 // 直接放普通元素
  14.                 tab[i] = newNode(hash, key, value, null);
  15.         } else {
  16.                 Node<K,V> e; K k;
  17.                 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
  18.                         // 存在同位元素,也就是出现了hash碰撞
  19.                         e = p;
  20.                 } else if (p instanceof TreeNode) {
  21.                         // 如果当前位置已经是红黑树节点,那么就put红黑色
  22.                         e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  23.                 } else {
  24.                         // 遍历哈希槽后面链接的其他元素(binCount统计的是插入新元素之前遍历过的元素数量)
  25.                         // 这里就是链表类型
  26.                         for (int binCount = 0; ; ++binCount) {
  27.                                 // 后继节点为空
  28.                                 if ((e = p.next) == null) {
  29.                                         // 拼接到后继节点上
  30.                                         p.next = newNode(hash, key, value, null);
  31.                                         /**
  32.                                          * 哈希槽(链)上的元素数量增加到TREEIFY_THRESHOLD后,这些元素进入波动期,即将从链表转换为红黑树
  33.                                          * 注意这个TREEIFY_THRESHOLD 是8,为什么是8??
  34.                                          * 每次遍历一个链表,平均查找的时间复杂度是 O(n),n 是链表的长度。由于红黑树有自平衡的特点,可以防止不平衡情况的发生,
  35.                                          * 所以可以始终将查找的时间复杂度控制在 O(log(n))。
  36.                                          * 最初链表还不是很长,所以可能 O(n) 和 O(log(n)) 的区别不大,但是如果链表越来越长,那么这种区别便会有所体现。所以为了提升查找性能,需要把链表转化为红黑树的形式。
  37.                                          * 链表查询的时候使用二分查询,平均查找长度为n/2,长度为8的时候,为4,而6/2 = 3
  38.                                          * 而如果是红黑树,那么就是log(n) ,长度为8时候,log(8) = 3, log(6) =
  39.                                          * 这个时候我们发现超过8这个阈值之后,链表的查询效率会越来越不如红黑树
  40.                                          */
  41.                                         if (binCount >= TREEIFY_THRESHOLD - 1) {
  42.                                                 // -1 for 1st
  43.                                                 treeifyBin(tab, hash);
  44.                                         }
  45.                                         break;
  46.                                 }
  47.                                 // 判断链表中的后继原始是否hash碰撞,如果发生了hash碰撞break
  48.                                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
  49.                                         break;
  50.                                 p = e;
  51.                         }
  52.                 }
  53.                 // 如果存在同位元素(在HashMap中占据相同位置的元素)
  54.                 if (e != null) { // existing mapping for key
  55.                         V oldValue = e.value;
  56.                         // 判断是否需要进行覆盖取值,因为key相同,那么直接取代,否则什么也不操作
  57.                         if (!onlyIfAbsent || oldValue == null) {
  58.                                 e.value = value;
  59.                         }
  60.                         afterNodeAccess(e);
  61.                         return oldValue;
  62.                 }
  63.         }
  64.         ++modCount;
  65.         if (++size > threshold)
  66.                 resize();
  67.         afterNodeInsertion(evict);
  68.         return null;
  69. }
复制代码
总结关键信息:
  1. 哈希槽(链)上的元素数量增加到TREEIFY_THRESHOLD后,这些元素进入波动期,即将从链表转换为红黑树
  2. 注意这个TREEIFY_THRESHOLD 是8,为什么是8??
  3. 每次遍历一个链表,平均查找的时间复杂度是 O(n),n 是链表的长度。由于红黑树有自平衡的特点,可以防止不平衡情况的发生,
  4. 所以可以始终将查找的时间复杂度控制在 O(log(n))。
  5. 最初链表还不是很长,所以可能 O(n) 和 O(log(n)) 的区别不大,但是如果链表越来越长,那么这种区别便会有所体现。所以为了提升查找性能,需要把链表转化为红黑树的形式。
  6. 链表查询的时候使用二分查询,平均查找长度为n/2,长度为8的时候,为4,而6/2 = 3
  7. 而如果是红黑树,那么就是log(n) ,长度为8时候,log(8) = 3, log(6) =
  8. 这个时候我们发现超过8这个阈值之后,链表的查询效率会越来越不如红黑树
复制代码
HashMap get,remove操作

除了红黑树的查找比较特殊,其余的链表查找就是暴力搜索,只是平均下来找到一个元素的话是n/2
  1. final Node<K,V> removeNode(int hash, Object key, Object value,
  2.                                                    boolean matchValue, boolean movable) {
  3.         Node<K,V>[] tab = table;
  4.         Node<K,V> p;
  5.         int n, index;
  6.         if (tab != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
  7.                 Node<K,V> node = null, e; K k; V v;
  8.                 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
  9.                         // 找到节点,并且是首节点
  10.                         node = p;
  11.                 } else if ((e = p.next) != null) {
  12.                         if (p instanceof TreeNode) {
  13.                                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
  14.                         } else {
  15.                                 // 链表查询,暴力搜索
  16.                                 do {
  17.                                         if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
  18.                                                 node = e;
  19.                                                 break;
  20.                                         }
  21.                                         p = e;
  22.                                 } while ((e = e.next) != null);
  23.                         }
  24.                 }
  25.                 // 移除节点,可能只需要匹配hash和key就行,也可能还要匹配value,这取决于matchValue参数
  26.                 if (node != null && (!matchValue || (v = node.value) == value ||
  27.                                                          (value != null && value.equals(v)))) {
  28.                         if (node instanceof TreeNode) {
  29.                                 // 移除红黑树节点
  30.                                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  31.                         } else if (node == p) {
  32.                                 // 移除首节点为后继节点
  33.                                 tab[index] = node.next;
  34.                         } else {
  35.                                 // 链表断开
  36.                                 p.next = node.next;
  37.                         }
  38.                         ++modCount;
  39.                         --size;
  40.                         afterNodeRemoval(node);
  41.                         return node;
  42.                 }
  43.         }
  44.         return null;
  45. }
复制代码
HashMap扩容

链表拆分,进入新的容器
这里有个知识点:如何使用位运算进行取模
a % b == a & (b - 1)
我们拆分链表的思路也是这样:比如原来长度为8的链表,也就是 x % 8 = x & (8 - 1) = x & 0111 也就是取后三位,那么扩容之后重新排序的话,容量扩大一倍,也就是16,那么这个时候就是 x % 16 = x & (16 - 1) = x & 1111 这个时候我们发现和之前的区别就是最高位由原来的0变为1,如果还在后三位范围内,那么新容量中的位置是不会变的
  1. final Node<K,V>[] resize() {
  2.     Node<K,V>[] oldTab = table;
  3.     int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4.     // 旧阈值
  5.     int oldThr = threshold;
  6.     int newCap, newThr = 0;
  7.     if (oldCap > 0) {
  8.         // 判断旧容量是否已经超过最大值
  9.         if (oldCap >= MAXIMUM_CAPACITY) {
  10.             // 如果已经达到1 << 30;,那么直接设置为Integer.MAX_VALUE;  0x7fffffff
  11.             threshold = Integer.MAX_VALUE;
  12.             return oldTab;
  13.         } else {
  14.             // mod by xiaof  尝试将哈希表数组容量加倍,注意这里是左移,也就是说*2
  15.             newCap = oldCap << 1;
  16.             // 如果容量成功加倍(没有达到上限),则将阈值也加倍
  17.             if (newCap < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
  18.                 newThr = oldThr << 1;
  19.             }
  20.         }
  21.         // else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  22.         //          oldCap >= DEFAULT_INITIAL_CAPACITY) {
  23.         //     newThr = oldThr << 1; // double threshold
  24.         // }
  25.     } else if (oldThr > 0) {
  26.         // initial capacity was placed in threshold
  27.         newCap = oldThr;
  28.     } else {               // zero initial threshold signifies using defaults
  29.         // 如果实例化HashMap时没有指定初始容量,则使用默认的容量与阈值
  30.         newCap = DEFAULT_INITIAL_CAPACITY;
  31.         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  32.     }
  33.     /*
  34.          * 至此,如果newThr==0,则可能有以下两种情形:
  35.          * 1.哈希数组已经初始化,且哈希数组的容量还未超出最大容量,
  36.          *   但是,在执行了加倍操作后,哈希数组的容量达到了上限
  37.          * 2.哈希数组还未初始化,但在实例化HashMap时指定了初始容量
  38.          */
  39.     if (newThr == 0) {
  40.         float ft = (float)newCap * loadFactor;
  41.         // 如果新容量小于最大允许容量,并且新容量*装载因子之后还是小于最大容量,那么说明不需要扩容,那么直接使用ft作为新的阈值容量
  42.         // 如果新容量已经超过最大容量了,那么就直接返回最大允许的容量
  43.         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  44.                   (int)ft : Integer.MAX_VALUE);
  45.     }
  46.     // 更新阈值
  47.     threshold = newThr;
  48.     // 新的容器对象,创建容量为新的newCap
  49.     @SuppressWarnings({"rawtypes","unchecked"})
  50.     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  51.     table = newTab;
  52.     if (oldTab != null) {
  53.         // 遍历原来的数据,准备转移到新的容器上
  54.         for (int j = 0; j < oldCap; ++j) {
  55.             // 获取旧容器对象
  56.             Node<K,V> e = oldTab[j];
  57.             if (e != null) {
  58.                 // 把原来的数组中的指针设置为空
  59.                 oldTab[j] = null;
  60.                 if (e.next == null) {
  61.                     // 重新计算hash索引位置,计算hash位置的方式防止数组越界的话,那么就设置hashcode & 长度 - 1
  62.                     newTab[e.hash & (newCap - 1)] = e;
  63.                 } else if (e instanceof TreeNode) {
  64.                     // 红黑树,这里是对红黑树进行拆分
  65.                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  66.                 } else { // preserve order
  67.                     // lo对应的链表是数据不会动的
  68.                     Node<K,V> loHead = null, loTail = null;
  69.                     // hi对应的链表标识是需要去新容器新的位置的
  70.                     Node<K,V> hiHead = null, hiTail = null;
  71.                     Node<K,V> next;
  72.                     // 这个是链表的情况下进行拆分
  73.                     // 因为num % 2^n == num & (2^n - 1),容量大小一定是2的N次方
  74.                     do {
  75.                         next = e.next;
  76.                         // 注意:e.hash & oldCap,注意这里是对老的容量oldCap进行计算这一步就是前面说的判断多出的这一位是否为1
  77.                         // 因为新的是老的2倍,新节点位置是否需要发生改变,取决于最高位是否为0
  78.                         // 若与原容量做与运算,结果为0,表示将这个节点放入到新数组中,下标不变
  79.                         // 由于原来的是2的倍数,那么取余肯定是和一个0111111的对象进行&操作,而不减一那就是10000000进行&操作,正好是最高位
  80.                         if ((e.hash & oldCap) == 0) {
  81.                             // 最高位为0,那么位置不需要改变,本身就在原来容量范围内的数据
  82.                             // 直接加入lotail,并判断是否需要初始化lotail
  83.                             if (loTail == null) {
  84.                                 loHead = e;
  85.                             } else {
  86.                                 loTail.next = e;
  87.                             }
  88.                             loTail = e;
  89.                         } else {
  90.                             // 最高位是1,那么就需要进行切换位置
  91.                             if (hiTail == null) {
  92.                                 hiHead = e;
  93.                             } else {
  94.                                 hiTail.next = e;
  95.                             }
  96.                             hiTail = e;
  97.                         }
  98.                     } while ((e = next) != null);
  99.                     if (loTail != null) {
  100.                         loTail.next = null;
  101.                         newTab[j] = loHead;
  102.                     }
  103.                     if (hiTail != null) {
  104.                         hiTail.next = null;
  105.                         newTab[j + oldCap] = hiHead;
  106.                     }
  107.                 }
  108.             }
  109.         }
  110.     }
  111.     // 最后返回最新的容器对象
  112.     return newTab;
  113. }
复制代码
afterNodeInsertion在linkedhashmap中作用不大
  1. Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
  2.         // 这里创建了linkedhashmap对象
  3.         LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
  4.         // 创建完成之后,就添加到链表中连接起来
  5.         linkNodeLast(p);
  6.         return p;
  7. }
  8. private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
  9.         LinkedHashMap.Entry<K,V> last = tail;
  10.         tail = p;
  11.         if (last == null)
  12.                 head = p;
  13.         else {
  14.                 p.before = last;
  15.                 last.after = p;
  16.         }
  17. }
复制代码
综上:linkedhashmap相对hashmap其实就是多加了一个链表把所有的数据关联起来,只有在遍历的时候才能体现出来有序,其他的操作是没有差别的
关于hashtable

首先hashtable是线程安全的,因为它所有的函数都加上了synchronized
链表头插法,没有红黑树的转换
初始化容量的时候默认是11,是奇数,而hashmap全都是2的幂次方
hashtable允许key为null
rehash函数
<blockquote>常用的hash函数是选一个数m取模(余数),这个数在课本中推荐m是素数,但是经常见到选择m=2n,因为对2n求余数更快,并认为在key分布均匀的情况下,key%m也是在[0,m-1]区间均匀分布的。但实际上,key%m的分布同m是有关的。

证明如下: key%m = key - xm,即key减掉m的某个倍数x,剩下比m小的部分就是key除以m的余数。显然,x等于key/m的整数部分,以floor(key/m)表示。假设key和m有公约数g,即key=ag, m=bg, 则 key - xm = key - floor(key/m)m = key - floor(a/b)m。由于0 [] newMap = new Entry[newCapacity];        modCount++;        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);        table = newMap;        // 重新把旧的原始转移到新数组上        for (int i = oldCapacity ; i-- > 0 ;) {                for (Entry old = (Entry)oldMap ; old != null ; ) {                        Entry e = old;                        old = old.next;                        // 这里因为容量是奇数,那么就需要使用%取余,而不是位运算 -》 a & (b - 1)                        int index = (e.hash & 0x7FFFFFFF) % newCapacity;                        e.next = (Entry)newMap[index];                        newMap[index] = e;                }        }}[/code]参考

https://www.cnblogs.com/tuyang1129/p/12368842.html  -- 链表拆分
https://www.cnblogs.com/lyhc/p/10743550.html - linkedhashmap
http://zhaox.github.io/algorithm/2015/06/29/hash

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
继续阅读请点击广告

本帖子中包含更多资源

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

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

水军大提督

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表