• HashMap源码学习和总结


    如何计算key的hash值

    static final int hash(Object key) {
       int h;
       return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    计算hashCode()后的h与h逻辑右移16位的结果异或作为hash值

    >>>:无符号右移,将h的二进制数整体右移16位,超出的去掉,左侧空位用0补齐(int型是4字节32位)

    举例:

    key="java" 

    h = key.hashCode() :3254818

    二进制:11 0001 1010 1010 0010 0010 (22位,不足32位)

    补齐完整的32位:0000 0000 0011 0001 1010 1010 0010 0010     ------ (1)

    h>>>16:0000 0000 0000 0000 0000 0000 0011 0001                  -------(2)

    ^ :对(1)和(2)异或操作,相同为0,不相同为1,可以理解为不进位相加,结果为0000 0000 0011 0001 1010 1010 0001 0011

    问题:为什么要设计 右移16位再异或的算法

    右移16位后,可以让h的低16位和高16位都参与运算,这样计算出来的最终的hash值更"散",更散意味着更小的hash碰撞,更低的hash冲突

    问题:异或(^)相比&,| 有什么优势?

    & 很明显,只有1 & 1的结果才是1,其他都是0,这样计算二进制结果肯定都向0 聚集

    | 也很明显,只要有1参与运算,结果都是1,只有0|0才是0,这样计算的二进制结果都会向1聚集

    ^ 不考虑2个二进制的先后位置,如果是0,说明2个位都是0或1,如果是1,说明两个分别是0和1,至于是谁的无所谓

    如何计算新key-value落在数组的哪个下标位置

    i = (n - 1) & hash

    这个结果实际就是hash%n   (n是2的x次幂)

    为什么不直接用hash%n ?因为取模运算的效率远低于&运算

    为什么每次数组扩容后长度设计为2的N次幂 ?因为设计为2的N次幂,才能使得(n-1)& h 与h%n相等,才能用 按位与 代替 % 运算,提高效率

    计算推导:

    先举一个例子

    100 / 8 = 12
    100 % 8 = 4
    使用二进制位运算,除以8[2 ^ 3]就是向右移了3位
    0110 0100 >> 3 = 0000 1100[100]
    0000 1100 = 12 就是商
    100 = 4 就是余数
    也就是0110 0100的后3位:100

    假设n=2 ^ m,除以2 ^ m就是右移m位,得到的结果就是商,移出来的m位二进制转10进制,就是余数(模)

    问题转化为:需要将商对应的位全部忽略掉,而余数对应的位数全部保留下来

    应该想到h & (00000111) ,就是说h需要和这样的二进制做运算:先是连续的0,后是连续的1,假设有K个连续的1

    根据数学里的等比数列通项公式,计算结果为1*2^0+1*2^1+1*2^2+........+1*2^(k-1)=2^k-1

    也就是说h要和2^k-1这样的数做&运算,这样的数二进制是 00...011...1,连续的0和连续的1的形式

    如果N是2^k,这个设计就OK了,因此,数组的长度扩容时总是为2的N次幂

     Node和TreeNode的结构

    单链表的节点

    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
    }

    红黑树的节点

      static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
            TreeNode<K,V> parent;  // red-black tree links
            TreeNode<K,V> left;
            TreeNode<K,V> right;
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            boolean red;
            TreeNode(int hash, K key, V val, Node<K,V> next) {
                super(hash, key, val, next);
            }
      }

    红黑树的定义 :自平衡二叉查找树,只有红黑节点,根和叶子节点都是黑节点,红的两个儿子节点都是黑,从任一节点到其叶子节点的每条路径上的黑节点个数相同

    插入,删除,查找的时间复杂度为O(logn)

    JDK8 链表尾插法

    for (int binCount = 0; ; ++binCount) {
         if ((e = p.next) == null) {
            p.next = newNode(hash, key, value, null);
            if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
               treeifyBin(tab,hash);
            } 
    break; } if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))){ break; } p = e; }

    p是这个for循环里的当前节点,初始值为p = tab[i = (n - 1) & hash],从头节点开始遍历

    newNode(hash, key, value, null)是要插入的节点

    当p.next==null的时候,插入尾巴,表明是尾插法

    当遇到某个节点key就是要插入的这个key时,直接break;

    否则继续迭代遍历

    关于JDK7中头插法导致链表成环的分析,这位老铁写的非常清楚细致:https://blog.csdn.net/fengyuyeguirenenen/article/details/122760014

    贴几个图理解下(网上画的比较清晰的图)

     

     

     

     线程不安全的问题

    线程安全问题,从状态溢出的角度着手,状态就是类的属性,主要是table数组:transient Node<K,V>[] table;

    put的时候value覆盖,代码位置为

     if ((p = tab[i = (n - 1) & hash]) == null)
          tab[i] = newNode(hash, key, value, null);

    逻辑比较清晰,判空后赋值操作,典型的线程安全问题

  • 相关阅读:
    python中is和==的区别
    深拷贝和浅拷贝
    编码和解码
    with语句处理异常
    python中运行flask报错:UnicodeDecodeError: 'utf8' codec can't decodebyte 0xd5 in position 0:invalid continuation byte
    python中update的基本使用
    python中的程序控制结构
    python中的四种存储结构总结
    python中list,tuple,dict,set特点对比总结
    解决UIScrollview无故偏移和导航条遮挡view的问题
  • 原文地址:https://www.cnblogs.com/yb38156/p/16489324.html
Copyright © 2020-2023  润新知