java hashMap详解

108 阅读10分钟

本文基于JDK1.8来分析
hashMap详解

  • 构造方法
//指定初始化的容量 不过new的过程并不会立即初始化 
public HashMap(int initialCapacity) {
   this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定初始化容量 以及加载因子 主要用来计算扩容时机
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);
}

//无参构造
public HashMap() {
  	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//传入map
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
  • put

    public V put(K key, V value) {
      	//具体调用putVal 同时对key进行hash计算
        return putVal(hash(key), key, value, false, true);
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    //举例来说明下 hash过程
    // 传入的 key为 111
    // 111的hashcode为48657 二进制 表示为 1011111000010001
    // 0000 0000 0000 0000 1011 1110 0001 0001 >>> 16 = 0000 0000 0000 0000 0000 0000 0000 0000
    // 0000 0000 0000 0000 1011 1110 0001 0001 ^ 0000 0000 0000 0000 0000 0000 0000 0000 =
    // 0000 0000 0000 0000 1011 1110 0001 0001
    // 0000 0000 0000 0000 1011 1110 0001 0001 转化为 10进制为48657
    
  • 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)
          //第一次put内容的时候 条件成立 肯定会进来
          //初始化table
          n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)	
          //计算索引对应的数组位置是否为null  为空的话 直接new一个node 放入对应的索引位置即可
          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))))
            //当前索引下的node节点的hash值  等于 传入的hash值 同时 node节点的key等于输入的key
            //将p赋值给e
            e = p;
          else if (p instanceof TreeNode)
            //当前索引对应的node为树节点  以树节点方式插入节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
            //当前索引位置下的node节点 存在链表
            for (int binCount = 0; ; ++binCount) {
              if ((e = p.next) == null) {
                //循环直到 当前索引位置的node的链表的最后一个 
                //创建新的node节点并赋值给当前node下的链表的 最后一个节点的下一个
                p.next = newNode(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                  //根据binCount 是否大于 8-1 来决定是否需要转化为红黑树
                  treeifyBin(tab, hash);
                //放入成功则退出循环 
                break;
              }
              if (e.hash == hash &&
                  ((k = e.key) == key || (key != null && key.equals(k))))
                // //当前node节点的hash值  等于传入的hash值 同时 node节点的key等于输入的key
                break;
              //将e 节点赋值给p节点
              p = e;
            }
          }
          if (e != null) { 
            //e节点不为空说明 存在和现在插入相同的节点
            //获取到e节点的 value
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
              //传入onlyIfAbsent为false   或者oldValue为空
              //进行覆盖旧的值
              e.value = value;
            //数据访问后回调
            afterNodeAccess(e);
            //返回旧的值
            return oldValue;
          }
        }
      	//计算修改数量
        ++modCount;
        if (++size > threshold)
          //++集合的大小 大于threshould 
          //进行扩容
          resize();
        afterNodeInsertion(evict);
        return null;
    }
    
  • resize

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //初始化逻辑进入时oldCap为0 
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
      	//这里不同的构造又有不用的计算方式
        //使用无参数构造 new HashMap<>(); oldThr = 0
      	//使用带初始化容量构造或者初始化容量和加载因子构造 threshold = tableSizeFor(初始化容量)= 将输入的容量向上转化为 2的N次		幂 输入为9  转化的后的值为 16  稍后在具体看这个方法  
        //
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
          	//旧数组容量大于0
            if (oldCap >= MAXIMUM_CAPACITY) {
              	//旧数组容量大于等于最大容量
              	//threshold 为integer最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
              	//新容量= 原容量*2  小于最大容量 并且 旧容量大于等默认初始化容量 
              	//则新的threshold 等于原threshold*2 
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) 
          	//使用带初始化容量构造或者初始化容量和加载因子构造 newCap = threshold
            newCap = oldThr;
        else {             
          	//使用无参构造时 newCap= 默认值 也就是16
            newCap = DEFAULT_INITIAL_CAPACITY;
          	//newThr = 16*0.75 = 12 
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
          	//newThreshold=0 时需要计算 
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
      	//将newThr 也就是 容量 * 加载因子 赋值给 threshold
        threshold = newThr;
      	//初始化新容量的Node数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      	//同时将newTab 赋值给table变量 
        table = newTab;
        if (oldTab != null) {
          	//初始化时oldTab 肯定为空的 不满足条件
          	//容量不够 进入到 扩容阶段  
            for (int j = 0; j < oldCap; ++j) {
              	//循环旧的node数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                  	//获取每个索引下标的元素
                  	//将旧的数组索引下标对应的元素 清空
                    oldTab[j] = null;
                    if (e.next == null)
                      	//node 元素的下一个节点为空  也就是不存在 hash冲突 没有形成链表 或者 树结构
                      	//重新计算旧的元素在新数组的索引位置 并放入
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                      	//如果为树的结构
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                      	//node节点下存在链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                      	//循环遍历链表节点
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                              	//节点的hashcode 和旧容量进行&
                                //为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;
                          	//同时将新数组的 当前索引位置+ 旧的容量 作为索引位置 赋值为高位的头节点
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
  • putTreeVal 放入树节点

    final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                   int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        for (TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0) {
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                         (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                         (q = ch.find(h, k, kc)) != null))
                        return q;
                }
                dir = tieBreakOrder(k, pk);
            }
    
            TreeNode<K,V> xp = p;
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node<K,V> xpn = xp.next;
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }
    
  • treeifyBin

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
          	//如果tab为空或者 table的长度小于 64 则进行扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
          	//获取对应数组索引下的node节点
            TreeNode<K,V> hd = null, tl = null;
            do {	
               	//将node替换为TreeNode
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    
    final void treeify(Node<K,V>[] tab) {
      TreeNode<K,V> root = null;
      for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
          x.parent = null;
          x.red = false;
          root = x;
        }
        else {
          K k = x.key;
          int h = x.hash;
          Class<?> kc = null;
          for (TreeNode<K,V> p = root;;) {
            int dir, ph;
            K pk = p.key;
            if ((ph = p.hash) > h)
              dir = -1;
            else if (ph < h)
              dir = 1;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0)
              dir = tieBreakOrder(k, pk);
    
            TreeNode<K,V> xp = p;
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
              x.parent = xp;
              if (dir <= 0)
                xp.left = x;
              else
                xp.right = x;
              root = balanceInsertion(root, x);
              break;
            }
          }
        }
      }
      moveRootToFront(tab, root);
    }
    

重点说一下几个地方

  • 计算hash的过程

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

    这里使用key的hashcode 和 key的hashcode 右移16位 进行异或运算

    最后获取元素应该保存的位置索引时 又进行 (hash(key)&(table.length-1) ) 来获取元素索引位置 为什么不直接使用元素的hashcode 和 (table.length-1) 做与运算 而是又是异或运算 又是右移运算的 个人理解 当数组容量小的时候 计算元素在数组中的位置

    hash&(table.length-1) 这样只用到了hash的低位 当不同的hash 低位相同 高位不同时会产生冲突 现在hash值将hashcode低16位与高16位进行异或(相同为0 不同为1)这样相当于混合了高低位 增加了随机性 减少了hash冲突 元素的分布更加随机

  • 扩容机制

    扩容出现的场景

    • 第一次存入数据 因为hashmap使用的为延迟初始化 即在放入元素的时候在进行判断
    • 存入元素后 将数组长度加+1 作为新的数组长度 如果 大于threshold 大于则进行扩容
  • 当node节点下存在链表的扩容

    { // preserve order
        //node节点下存在链表
        Node<K,V> loHead = null, loTail = null;
        Node<K,V> hiHead = null, hiTail = null;
        Node<K,V> next;
        //循环遍历链表节点
        do {
          next = e.next;
          if ((e.hash & oldCap) == 0) {//注释4
            //节点的hashcode 和旧容量进行&运算
            //构建为索引不变的链表的头
            if (loTail == null) //注释5
              loHead = e;  //注释7
            else
              //注释6
              loTail.next = e;
            //
            loTail = e;
          }
          else {
            //构建为索引改变的链表头 新的索引 = 原先索引+ 旧table的容量
            if (hiTail == null)
              hiHead = e;
            else
              hiTail.next = e;
            hiTail = e;
          }
        } while ((e = next) != null);
        if (loTail != null) { //注释8
          //索引不变的链表的尾结点不为空 
          //索引不变的链表的尾结点赋值为空
          loTail.next = null;
          //同时将新数组的 当前索引位置 赋值为索引不变的链表的头
          newTab[j] = loHead;
        }
        if (hiTail != null) {
          //索引改变的链表的尾结点不为空
          //索引改变的链表的尾结点下一个为空
          hiTail.next = null;
          //同时将 索引改变后的链表头结点 放到新的table 新的索引位置上
          //新索引的位置 =  原先索引位置 + 旧table的容量
          newTab[j + oldCap] = hiHead;
        }
    }
    

    正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。

    hash值的每个二进制位用abcde来表示,那么,hash和新旧table按位与的结果,最后4位显然是相同的,唯一可能出现的区别就在第5位,也就是hash值的b所在的那一位,如果b所在的那一位是0,那么新table按位与的结果和旧table的结果就相同,反之如果b所在的那一位是1,则新table按位与的结果就比旧table的结果多了10000(二进制),而这个二进制10000就是旧table的长度16。

    换言之,hash值的新散列下标是不是需要加上旧table长度,只需要看看hash值第5位是不是1就行了,位运算的方法就是hash值和10000(也就是旧table长度)来按位与,其结果只可能是10000或者00000。

    所以,注释4处的e.hash & oldCap,就是用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标就等于原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度。

    理解了上面的原理,这里的代码就好理解了,代码中定义的四个变量:

    loHead,下标不变情况下的链表头

    loTail,下标不变情况下的链表尾

    hiHead,下标改变情况下的链表头

    hiTail,下标改变情况下的链表尾

    而注释4处的(e.hash & oldCap) == 0,就是代表散列下标不变的情况,这种情况下代码只使用了loHead和loTail两个参数,由他们组成了一个链表,否则将使用hiHead和hiTail参数。

    其实e.hash & oldCap等于0和不等于0后的逻辑完全相同,只是用的变量不一样。

    以等于0的情况为例,处理一个3–>5–>7的链表,过程如下:

    首先处理节点3,e3,e.next5

    1,注释5,一开始loTail是null,所以把3赋值给loHead。

    2,注释7,把3赋值给loTail。

    然后处理节点5,e5,e.next7

    1,注释6,loTail有值,把e赋值给loTail.next,也就是3.next==5。

    2,注释7,把5赋值给loTail。

    现在新链表是3–>5,然后处理节点7,处理完之后,链表的顺序是3–>5–>7,loHead是3,loTail是7。可以看到,链表中节点顺序和原链表相同,不再是JDK1.7的倒序了。

    代码到注释8这里就好理解了,

    只要loTail不是null,说明链表中的元素在新table中的下标没变,所以新table的对应下标中放的是loHead,另外把loTail的next设为null

    反之,hiTail不是null,说明链表中的元素在新table中的下标,应该是原下标加原table长度,新table对应下标处放的是hiHead,另外把hiTail的next设为null。