HashMap1.8源码分析

面试必备:HashMap(JDK1.8)原理以及源码分析 对于HashMap想必大家都不陌生,无论是平时code还是面试都经常和它打交道。今天我们通过源码的层面来分析一下它的实现原理,注意本文基于的是JDK1.8。 问题是从哪边开始聊起呢?我觉得不妨先从一段熟悉的代码开始。 Map<Integer, String> map = new HashMap<Integer, String>();map.put(1, "Jack"); 然后我们会迫不及待点开HashMap这个类,发现里面有大量的属性和方法,一脸懵逼。那就直接点开put. 阅读详情

HashMap数据结构

数组+链表  1.7
数组+链表+红黑树 1.8

HashMap为什么要用链表?

解决hash冲突

为什么要用红黑树?(查找接近二分查找)

方便查询(时间复杂度低)

HashMap的容量为什么要为2的次方数呢?

1.hash运算方便(在进行hash运算的时候可以通过位运算来进行,不必进行取模,提高效率)
2.在数组在扩容的时候,方便计算新的位置

hash函数

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

这里的hash函数和1.7的不同,因为1.8新增了红黑树这个结构,所以不用像1.7的那么复杂.
这种做法就是将hashCode()返回的hash值异或上该值右移16位.就是让高位参加运算,增加散列性.
这样hash的好处还有一点: 
在数组进行扩容的时候, 旧数组table[i]位置的所有值在转移到新数组的位置只会有两种情况:
1.在新数组的 newTable[i]位置
2.在新数组的 newTable[i+oldCap] 位置

为什么会这样呢?

举个例子:
有两个hashCode : e.h1 =  1010 1001    e.h2 = 1101 1001  (低4位相同,高位不同)
数组长度:  table.length = 16
该值在table的存储位置为 e.h ^ (16 -1) 
计算e.h1: 
  1 0 1 0   1 0 0 1       e.h1
& 0 0 0 0   1 1 1 1       15
= 0 0 0 0   1 0 0 1  =  9  所以放入table[9]
此时数组扩容 table.length = 2 * 16 =32
再次计算:         e.h1 ^ (32 -1) 
  1 0 1 0   1 0 0 1       e.h1
& 0 0 0 1   1 1 1 1       31
= 0 0 0 0   1 0 0 1  =  9  所以放入newTable[9]   table[i]的数放入 newTable[i]

计算e.h2:
  1 1 0 1   1 0 0 1       e.h2
& 0 0 0 0   1 1 1 1       15
= 0 0 0 0   1 0 0 1  =  9  所以放入table[9]
此时数组扩容 table.length = 2 * 16 =32
再次计算:         e.h2 ^ (32 -1) 
  1 1 0 1   1 0 0 1       e.h2
& 0 0 0 1   1 1 1 1       31
= 0 0 0 1   1 0 0 1  =  25  所以放入newTable[25]  table[i]的数放入newTable[i+oldCap]

发现再扩容之后的低四位运算不变,只是多了一位参与运算,而这一位计算的结果只有0或1, 0代表index运算结果和原先结果一样,1则代表增加了 多出来计算的那一位的值,由例子来看,在计算中新增的那一位是 16 ,所以index+新增的那一位的值即可,也可以看做是增加了原先的数组长度.

JDK 1.8 HashMap分析

属性:

//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; 
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的大小等于或大于8,就会变成树结构 (变树的条件之一)
static final int TREEIFY_THRESHOLD = 8; 
//当树的大小小于或等于6,就会变成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化容量阈值为,当表的长度(数组的长度)>该值,才允许将链表树化
//否则,桶内元素过多时直接扩容,而不是树形化
//了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64; (table.length>this : 是变树的第二个条件)
//主要数据结构
transient Node<K,V>[] table;

//Node节点
class Node<K,V> implement Map.Entry{
    int hash;
    K key;
    V value;
    Node<K,V> next;
}
/**TreeNode继承了LinkedHashMap.Entry ,
而Entry又继承了HashMap.Node,所以他会有HashMap的Node的属性next,
自己也有prev属性,所以在是一颗树的同时也维护了一个双向链表.
**/
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;
}
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}


构造函数

public HashMap(int initialCapacity, float loadFactor) {
    //如果给定容量小于0,则不合法,抛出异常 
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果给定容量大于最大容量2<<30, 则初始化容量为最大容量 
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //判断加载因子的合法性
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //赋值给HashMap的属性
    this.loadFactor = loadFactor;
    //这里需要对对传入的容量大小进行转化,会转为大于或等于该值的最小2次方数 
    //例: initialCapacity = 10,  进行tableSizeFor()方法会变为 16 因为 10 < 2^4=16
    this.threshold = tableSizeFor(initialCapacity);
}
//该方法的目的就是将传入的数转为大于或等于该值的最小2次方数 
static final int tableSizeFor(int cap) {
    //先将该值-1
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

以二进制的形式进行运算
例:i=18
18 = 0001 0010
(1)  0001 0010右移一位:  0000 1001
     0001 0010 或 0000 1001 :  0001 1011
(2) 0001 1011 右移2位: 0000 0110 
     0001 1011 或 0000 0110 :0001 1111
(3)0001 1111 右移4位: 0000 0001
     0001 1111 或 0000 0000 : 0001 1111
(4)0001 1111 右移8位:0000 0000
     0001 1111 或 0000 0000 :0001 1111
(5)0001 1111 右移16位:0000 0000
     0001 1111 或 0000 0000 :0001 1111
此时结果为 0001 1111 也就是31 最后进行加1也就会变成 32.因为  18< (2^6=32)
右移与或运算的目的就是想让某个数字的低位都变为1,再用该结果 加1 ,即得到了想要的结果。

问题:HashMap是在你new 的是否就已经创建(为数组分配空间)好了吗?
答案:不是这样的,在分析源码的时候,HashMap的构造函数上面没有对table属性进行操作.那是在什么时候进行初始化table的呢.

不妨可以调试一下

HashMap<String,String> map = new HashMap();
map.put("1","2");

我们可以点进去put方法里面看一下put的过程:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
他首先调用了putVal()方法.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //创建一个tab数组,一个p引用,两个变量
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //判断table[i]位置的key是否相等,如果相等,将节点e指向该位置
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //判断当前位置的结构是否为 树
        else if (p instanceof TreeNode)
            //将节点插入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果不是以上情况,那么只能是一个链表了
        else {
            //定义一个变量,用来记录遍历的节点个数
            for (int binCount = 0; ; ++binCount) {
                //如果p.next不为null , 不进入第一个if
                if ((e = p.next) == null) {
                    //如果p.next==null ,说明遍历到了最后一个节点
                    //创建一个新节点,进行尾插法
                    p.next = newNode(hash, key, value, null);
                    //需要判断bigCount是否大于7(链表长度过长需要进行树化,而树化的要求是链表的长度>8)
                    //因为bigCount是从0开始的.
                    //这里需要思考一个问题:是插入第8个元素的时候会树化,还是插入第9个元素时树化?
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //判断p.next的key是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //相当于 p = p.next(因为e = p.next)
                p = e;
            }
        }
        //判断e是否存在
        if (e != null) { // existing mapping for key
            //如果存在,获取旧值
            V oldValue = e.value;
            //判断是否需要更新该值.putVal()的参数onlyIfAbsent为false,所以需要更新该值
            //key相同,value值需要进行覆盖操作
            //可以在使用map.putIfAbsent("1","2");方法,表示如果key相同,不进行任何更新覆盖操作.
            if (!onlyIfAbsent || oldValue == null) 
                e.value = value;
            afterNodeAccess(e); //子类用到的方法
            //返回旧值
            return oldValue;
        }
    }
    //添加数据成功,版本号+1
    ++modCount;
    //判断新的size是否大于扩容阈值,如果大于则需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); //子类用到的方法
    return null;
}

可以看出put的核心代码在这里.来分析一下.

  1. 在put的时候,第一件事就是去检查这个table是否为null,因为构造函数没有对table进行初始化,所以第一次调用put,table==null ,在这个时候,调用resize()方法,才进行初始化table;
  2. 数组初始化完成之后,通过hash & (table.length - 1)找出要插入的数组下标(相当于1.7的indexFor(hash,table.length)),找到位置后,判断当前位置是否为null,第一次插入肯定为null,所以直接new一个节点放到table[i]位置即可,不进入else ,modcount++(对HashMap的修改次数,也叫版本号)
  3. 进行size加一操作,然后判断size(数组存放的所有数据,包括链表存放的)是否大于阈值threshold.如果成立,则需要进行resize()扩容.(在上面初始化的时候也用到了resize(),该方法可用于初始化,同样也可用于扩容).

上面有一个问题:就是在插入第8个元素的时候会树化,还是插入第9个元素时树化?

for (int binCount = 0; ; ++binCount) {
      //如果p.next不为null , 不进入第一个if
      if ((e = p.next) == null) {
          //如果p.next==null ,说明遍历到了最后一个节点
          //创建一个新节点,进行尾插法
          p.next = newNode(hash, key, value, null);
          //需要判断bigCount是否大于7(链表长度过长需要进行树化,而树化的要求是链表的长度>8)
          //因为bigCount是从0开始的.
          //这里需要思考一个问题:是插入第8个元素的时候会树化,还是插入第9个元素时树化?
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
               treeifyBin(tab, hash);
               break;
          }
          //判断p.next的key是否相等
          if (e.hash == hash &&
              ((k = e.key) == key || (key != null && key.equals(k))))
               break;
          //相当于 p = p.next(因为e = p.next)
          p = e;
         
}

可以举例分析一下,因为bigCount是从0开始的,所以当我数组中有7个节点的时候,遍历完成之后++bigCount最终 = 6,接着进入if(p.next == null) ,创建新节点,插入第8个节点:p.next = newNode; 此时会去判断bigCount是否 >= 7,在这个时候,因为if()语句还没执行完,bigCount = 6 ,而现在链表的节点个数为8,因为刚刚新插入了一个节点.bigCount < 7不满足树化条件.可以看出,当插入第8个节点(元素)的时候,并没有进入if()语句,也就不会树化.
继续分析,此时链表有8个节点,最终++bigCount = 7,接着将第九个节点插入,此时判断bigCoung是否 >= 7,7 = 7 条件成立!

所以得出结论,在插入第9个节点的时候才会进行树化,也就是说当链表长度>8的时候,会进行链表树化(当然也不是绝对的,链表长度 > 8只是树化条件之一)

在putVal()方法里面有一个判断是判断bigCount >= 7(判断是否需要进行树化)
如果条件满足,来看一下方法 treeifyBin(Node<K,V>[] tab, int hash)

final void treeifyBin(Node<K,V>[] tab, int hash) {
    //定义几个变量
    int n, index; Node<K,V> e;
    //这里是关键*************
    //1.首先会判断数组是否为空,会返回false ,因为是 || 操作,所以前面为false还需要判断后面
    //2.判断数组的长度是否小于MIN_TREEIFY_CAPACITY该值. 该值为64
    //所以是否需要树化有两个条件
    // 1.当前table[i]位置链表长度是否大于8
    // 2.table的长度是否大于 64. 
    //两者需要同时满足的情况下,才会进行链表树化,否则只会进行数组的扩容resize()
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) { 
        //接下来要做的不是链表树化,而是将该table[i]下的链表组成一个双向链表
        //定义一个TreeNode头结点 hd, 和辅助节点 t1
        TreeNode<K,V> hd = null, tl = null;
        do {
            //replacementTreeNode()该方法返回一个TreeNode节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            //第一步将头结点指向Node链表的第一个节点
            if (tl == null)
                hd = p;
            else {
                //p相当于遍历Node链表的节点
                //将链表的节点插入到该双向链表TreeNode的后面
                p.prev = tl;
                tl.next = p;
            }
            //辅助t1指向p
            tl = p;
        } while ((e = e.next) != null);
        //将该双向链表放入table[i]位置,如果不为空,进行树化操作
        if ((tab[index] = hd) != null)
            //真正的链表树化操作
            hd.treeify(tab);
    }
}

从这个do{}while操作可以发现,它将原先的Node链表变成了一个双向的TreeNode链表.
也就是说后面树化操作形成的树 也是一个双向链表.

注意:链表的树化是为了方便查询,而数组的扩容,会让数据更加散列化,效果是一样的.

来看一下resize()方法到底是怎么实现的?

  • resize()初始化流程:
final Node<K,V>[] resize() {
    //首先创建一个数组指向原数组
    Node<K,V>[] oldTab = table;
    //判断原数组是否为null(这里在第一次put的时候数组还未初始化,所以为null,接下来进行初始化)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取原来的扩容阈值
    int oldThr = threshold;
    //定义两个变量
    int newCap, newThr = 0;
    //判断原数组的长度是否大于 0 (扩容的时候会进入该if)
    if (oldCap > 0) {
        //判断数组旧长度是否大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            //大于最大容量,则将该值赋值给扩容阈值
            threshold = Integer.MAX_VALUE;
            //直接返回旧长度,不能再扩容了
            return oldTab;
        }
        //旧长度小于最大容量
        //计算新容量newCap,odlCap<<1 也就是旧数组长度的2倍.
        //判断新数组的长度是否小于最大容量,如果成立
        //接着判断旧数组的大小是否大于默认初始容量,也就是16
        //如果成立,则设置新数组的扩容阈值为旧数组的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //旧数组长度<=0 ,判断旧的扩容阈值是否大于0
    else if (oldThr > 0) // initial capacity was placed in threshold
        //将新值设置为旧值
        newCap = oldThr;
    //初始化操作
    else {               // zero initial threshold signifies using defaults
        //设置新的容量为默认容量 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //计算新的扩容阈值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //设置新值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //初始化新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将新数组赋值给原来的数组
    table = newTab;
    //以上是初始化工作
    //底下的都是关于扩容操作
    ..........
}
  • resize()扩容:
    核心代码
final Node<K,V>[] resize() {
    //首先创建一个数组指向原数组
    Node<K,V>[] oldTab = table;
    //判断原数组是否为null(这里在第一次put的时候数组还未初始化,所以为null,接下来进行初始化)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取原来的扩容阈值
    int oldThr = threshold;
    //定义两个变量
    int newCap, newThr = 0;
    //判断原数组的长度是否大于 0 (扩容的时候会进入该if)
    if (oldCap > 0) {
        //判断数组旧长度是否大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            //大于最大容量,则将该值赋值给扩容阈值
            threshold = Integer.MAX_VALUE;
            //直接返回旧长度,不能再扩容了
            return oldTab;
        }
        //旧长度小于最大容量
        //计算新容量newCap,odlCap<<1 也就是旧数组长度的2倍.
        //判断新数组的长度是否小于最大容量,如果成立
        //接着判断旧数组的大小是否大于默认初始容量,也就是16
        //如果成立,则设置新数组的扩容阈值为旧数组的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }    
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //设置新扩容阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建新的数组,大小为2*OldCap
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //原数组指向新数组
    table = newTab;
    //进行数据的转移操作
    if (oldTab != null) {
        //遍历原数组
        for (int j = 0; j < oldCap; ++j) {
            //定义一个辅助节点
            Node<K,V> e;
            //遍历每个table[i]位置的数据
            if ((e = oldTab[j]) != null) {
                //将老数组[j]位置置为null, help GC
                oldTab[j] = null;
                //如果e.next == null ,则说明该位置只有一个节点
                if (e.next == null)
                    //直接计算出在新数组的位置,然后放入即可
                    newTab[e.hash & (newCap - 1)] = e;
                //不止一个节点, 判断是否是一棵树
                else if (e instanceof TreeNode)
                    //进行树的转移,下面会说到
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //到了这个位置就只有一种可能,是链表
                //这个时候需要注意hash & table.length-1的好处了,在上面hash函数说到,
                //原先table[i]位置上的节点再扩容之后只会有两个位置
                //1. newTable[i]
                //2. newTable[i+OldCap]
                else { // preserve order
                    //与1.7不同的是,因为旧节点在新数组的下标只能有两个
                    //就用两个链表将newTable[i]和 newTable[i+OldCap]位置的数据链起来
                    //loHead指向将插入newTable[i]的节点
                    //hiHead指向将插入newTable[i+OldCap]的节点
                    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) {
                            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);
                    //如果链表不为空,则直接将链表头部放入新数组newTable[i]位置上
                    //链表为空则旧数组的节点没有要放入newTable[i]位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //同上操作,两者只能有一个为空(要么全部放入newTable[i]位置上,
                    //        要么全部放入newTable[i+OldCap]位置上)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回新数组
    return newTab;
}

树的数据转移过程:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //先遍历原先树,用两个链表将数据连接起来.
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
            	//1.loHead != null -> 节点长度是否小于等于6(因为树的节点个数小于6要将树转为链表).
                if (lc <= UNTREEIFY_THRESHOLD)
                	//若小于等于6,则将loHead变为一个**Node**类型的链表(**loHead定义为TreeNode类型**).
                    tab[index] = loHead.untreeify(map);
                else {
                	//将table[i] = loHead; 接着判断hiHead是否为空,若不为空则将loHead变为TreeNode类型的树.
                    tab[index] = loHead;
                    //(注意:hiHead如果为空,则loHead就相当于原先的那棵完整的树,不需要在转化为树.
					//而hiHead不为null,则loHead为原树的一部分数据,而loHead本身为链表的头,所以要转为一棵树). 
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            //同上
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }
//将TreeNode类型转为Node类型
final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
            	//返回 q的Node节点类型
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

扩容过程:
1.获取原先table数组长度oldCap,扩容阈值oldTr
2.二倍扩容newCap = oldCap << 1,newTr = oldTr << 1
3.创建一个新数组newTab = new Node[newCap],将原先数组指向新数组
4.进行数据转移 (三种情况)

  • 4.1.数组table[i]位置只有一个元素:直接将数据转移到新数组的位置(newIndex = e.hash & (newCap-1))
  • 4.2.table[i]存放的是一棵树:同链表转移一样,上面说过,树其实也是一个双向链表,因为旧数组table[i]位置上的元素转移到新数组时存放的位置只有两种:一种是newTab[i],另一种是newTab[ i + oliCap].所以会发现定义了四个TreeNode节点:loHead,loTail和hiHead,hiTail,分别代表即将存在新数组两个位置的节点头和尾.
    • 4.2.1 :然后遍历链表,计算节点在新数组的下标值(e.hash & oldCap),根据根据下标值确定节点添加在loHead还是 hiHead后面.对loHead和hiHead的节点个数进行记录.最后loHead链表连接的节点是要插入newTab[i]位置的,而hiHead链表连接的节点是要插入newTab[i+oldCap]位置的.
    • 4.2.2 :这个时候链表loHead和hiHead可能会有一个为空,因为有可能节点在新数组的位置经过计算后都在一个位置,所以要进行判断:1. loHead != null -> 节点长度是否小于等于6(因为树的节点个数小于6要将树转为链表).若小于等于6,则将loHead变为一个Node类型的链表(loHead定义为TreeNode类型).将table[i] = loHead; 接着判断hiHead是否为空,若不为空则将loHead变为TreeNode类型的树.(注意:hiHead如果为空,则loHead就相当于原先的那棵完整的树,不需要在转化为树.而hiHead不为null,则loHead为原树的一部分数据,而loHead本身为链表的头,所以要转为一棵树). 2. hiHead != null 同上.
  • 4.3.table[i]存放的是一个链表,其实和树转移操作类似,定义四个Node类型的节点:lohead,loTail,hiHead,hiTail,计算e.hash & OldCap,若等于0,则e将会存放在新数组的newTab[i]位置,用loHead将节点连接起来.等于1,则e将会存放在新数组 newTab[i+oldCap]位置,用hiHead将节点连接起来.最后将loHead,和hiHead放入新数组对应的位置即可.

删除方法

remove()方法

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //判读数组是否为空,和table[i]位置是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node指向待删除节点
            Node<K,V> node = null, e; K k; V v;
            //判断table[i]位置的节点是否是要删除的
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //如果是,将临时节点node指向 table[i]位置节点 p
                node = p;
            //不止一个元素
            else if ((e = p.next) != null) {
            	//该位置存放的是一棵树
                if (p instanceof TreeNode)
                	//从树里面查找
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                	//该位置存放的是一个链表
                    do {
                    	//遍历链表
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //判断是否找到了待删除节点
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果待删除节点在树里
                if (node instanceof TreeNode)
                	//从树里面移除该节点
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                	//如果待删除节点是第一个元素,将p.next放入table[i]
                    tab[index] = node.next;
                else
                	//存在链表里面,将待删除节点node前驱的next->node.next
                    p.next = node.next;
                //版本号加1(修改次数)
                ++modCount;
                //数据个数-1
                --size;
                afterNodeRemoval(node);
                //返回删除的节点
                return node;
            }
        }
        //没有找到或者,找到了但不进行任何操作,返回null,和putAbsent一个道理
        return null;
    }
【SeedLab】Packet Sniffing and Spoofing Lab 本节实验要求:借助Scapy库,实现网络流量的捕获与伪造。 阅读详情

相关推荐

从官方例程到实战:AXI Memory Mapped To PCIe 核在FPGA-RC模式下的配置与调试全解析

本文详细解析了AXI Memory Mapped To PCIe核在FPGA-RC模式下的配置与调试方法。从IP核基础结构到实战参数设置,再到例程代码剖析和调试技巧,全面覆盖了AXI与PCIe协议转换的关键技术点,帮助开发者高效实现FPGA与PCIe设备的高速数据交互。

weixin_42550597的博客 270

hashmap 的 hash()方法详解:(h = key.hashCode()) ^ (h >>> 16)

看下该方法: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 返回的是(h = key.hashCode()) ^ (h >>> 16)的值,也就是hashcode和无符号右移16位的hashcode后得到的值做异或运算 h = key.hashCode()的时候已经把hashcode

EnseHeiKe的博客 5328

保姆级教程:用GlobleLand30数据+GTB3.3软件,一步步搞定MSPA景观格局分析

本文提供GlobleLand30数据结合GTB3.3软件进行MSPA景观格局分析的保姆级教程,涵盖数据预处理、软件操作、结果解读全流程,特别针对零基础用户常见问题提供解决方案,助力研究者高效完成景观生态学分析。

weixin_26757925的博客 450

h = key.hashCode()) ^ (h >>> 16) 详细解读以及为什么要将hashCode值右移16位并且与原来的hashCode值进行异或操作

h = key.hashCode()) ^ (h >>> 16) 详细解读以及为什么要将hashCode值右移16位并且与原来的hashCode值进行异或操作

小爽帅到拖网速的博客 7606

VTK基础教程(1)- vtkPolyData 介绍

目录 VTK的编译与环境搭建 VTK一般渲染流程 VTK数据类型-vtkPolyData 使用vtk自带的多边形数据 使用自己创建的数据 添加顶点颜色 添加面颜色 VTK的编译与环境搭建 这部分目前已经有了大佬做了成熟的傻瓜式配置,大家可以参考这篇文章:VTK+VS2019(或其他版本均可) 傻瓜式一站式 配置_Nancy_fairy的博客-优快云博客_vs2019 vtk VTK一般渲染流程 这部分的介绍也很多,大家可以参考这篇文章:VTK笔记-了解VTK_黑山老妖的博客.

qq_35769071的博客 9055

IBTrACS v04r01 数据提取实战:Pandas 筛选 JTWC/CMA/JMA 3机构最佳路径数据

本文详细介绍了如何使用 Pandas 高效提取和处理 IBTrACS v04r01 数据集中的最佳路径数据,特别针对 JTWC、CMA 和 JMA 三家权威机构的数据进行对比分析。通过构建可复用的数据提取函数,解决多机构数据质量差异问题,并提供高级分析与可视化应用示例,助力热带气旋研究。

clijovtbq401783153的博客 517

为什么 HashMap 要用 h^(h >>>16) 计算hash值?槽位数必须是 2^n?

大家好,我是一航! 昨天中午,一位粉丝朋友在微信私信我,问:为啥HashMap的hash值计算格式是这样:(h = key.hashCode()) ^ (h >>> 16)?h ^ (h >>> 16)是什么意思? 以下是Java8中HashMap计算key对应hash的源码: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode().

一行Java的博客 5650

Linux下自动化解析q7签名文件的有效期与时间戳

本文详细介绍了在Linux环境下自动化解析q7签名文件有效期与时间戳的方法。通过编写高效脚本,开发者可以批量提取签名时间信息,验证签名有效性,并集成到CI/CD流程中。文章提供了完整的脚本实现,包括错误处理、批量处理功能,并解决了常见环境问题,特别适用于国产操作系统如UOS的开发场景。

weixin_30872499的博客 521

HashMap

HashMap 简介 HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。 JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。 底层数据...

luoru的博客 354

【Python搞定车载自动化测试】——Python实现车载以太网DoIP刷写(含Python源码)

随着智能电动汽车行业的发展,汽车=智能终端+四个轮子,而人工智能也发展越来越快,Python语言使用越来越广,本章节主要介绍如何使用Python语言基于Pytest自动化框架来实现DoIP诊断自动化测试。首先回顾一下DoIP诊断基础知识:DoIP(Diagnosis over Internet Protocol)是一种用于车辆诊断的网络通信协议。它基于现代互联网技术,允许通过以太网或IP网络进行车辆诊断和通信。

测试领域技术分享 3797

数据结构之博弈树搜索(深度优先搜索)

数据结构之博弈树搜索(深度优先搜索)

LucianaiB的博客 1527

HashMap添加、转换为红黑树、扩容源码分析

HashMap添加、转换为红黑树、扩容源码分析 put方法 通过hashcode计算出key映射到哪个桶 如果没有发生hash冲突直接插入 如果发生了hash冲突久处理冲突 如果使用的是红黑树处理冲突,那么就调用红黑树的方法插入数据 2. 如果是链表方法插入,那么就按照链表方式插入,如果达到了临界值就把链表转换为红黑树 4.如果桶中存在重复的键,那么就替换value 5.如果size大于预值,那么就扩容 public V put(K key, V value) { return

qq_40102411的博客 787

Python爬虫实战:如何通过解析JSON数据抓取抖音视频评论

本文介绍了如何使用Python爬虫抓取抖音视频评论数据。请求头伪造:通过使用随机的User-Agent,模拟真实用户的请求。代理池:使用代理IP来避免被封禁。JSON解析:直接从API接口获取JSON格式的数据,避免了动态加载内容的限制。需要注意的是,抓取网站数据时应遵守网站的爬虫协议,避免过度抓取影响网站性能或违反法律法规。希望本文能够帮助你掌握如何抓取和分析抖音视频评论数据!

2201_76125261的博客 3362

JDK1.8 HashMap原理及源码分析

概述 JDK1.8对HashMap底层的实现进行了优化,引入红黑树的数据结构和扩容的优化等。   HashMap源码分析 HashMap的put方法执行过程: putVal()源码分析: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node&lt;K, V&gt;...

工作记录 802

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

HashMap源码分析(JDK1.8)

本文转自:https://www.cnblogs.com/xiaoxi/p/7233201.html,本来准备自己写的,但是自己理解的不够透彻,后来通过百度找到了这篇文章,原作者可以说写的很用心,分析的很到位,看完之后让我受益匪浅。 一、HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一...

keyto1的博客 174

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

java HashMap源码分析(JDK1.8)

HashMap继承AbstractMap<K,V> ,implements Map<K,V>, Cloneable, Serializable 键值对存储,HashMap最多只允许一条记录的key值为Null(多条会覆盖);允许多条记录的Value为 Null. 源码中可以看到一个静态的内部类(单向链表):static class Node<K,V> imp...

来杯Java 1574

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

JDK1.8HashMap源码分析

JDK1.8 HashMap源码分析

a2231476020的博客 184

JDk1.8HashMap的源码分析

JDk1.8HashMap的源码分析 HashMap用数组存放数据(总结) 调用键的hashCode()获得键的哈希值 用哈希值,计算一个下标值 i 新建Entry对象来封装键和值 Entry对象放在 i 位置 空位置,直接放入 有数据,依次用equals()比较是否相等 找到相等的,覆盖值 没有相等的,链表连接在一起 负载率、加载因子到0.75 新建翻倍容量的新数组 所有数据,重新哈希,放...

smileLs的博客 147

HashMap源码分析1.8

HashMap1.8的升级主要是修复1.8的问题: 目录 1.重要源码分析 2.结构组成? 3.如何解决在扩容的时候碰到的链表死循环的问题 4.如何解决时间复杂度从O(0)-->O(n)的过程 1.重要源码分析 /* * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to li

Mr_xiayijie的博客 317

hashmap源码分析、扩容过程,1.7与1.8的区别

Java:手把手带你源码分析 HashMap 1.7https://blog.csdn.net/carson_ho/article/details/79373026 Java源码分析:关于 HashMap 1.8 的重大更新https://blog.csdn.net/carson_ho/article/details/79373134 ...

【小石头的茅坑】 71万+

HashMap 1.8版本的原理介绍以及源码分析 1.7与1.8版本的区别及改进

HashMap原理介绍 HashMap 的结构是 数组 + 链表 + 红黑树; 首先HashMap在初始化时并不会申请连续的存储空间,而是在存数据(put)时申请,这样做是为了避免内存空间的浪费。 在初始化后,我们第一次向数组中put键值对时,需要让key值经过一个扰动函数(哈希函数),目的是为了让key值更加散列化,在极大程度上避免重复。key值经过扰动后将得到32位的固定长度的二进制值;将这...

quique的博客 457

JDK源码分析——HashMap(JDK1.8)

1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap。HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化。另外,需要注意的是,HashMap 是非线程安全类,在多...

小马不奔腾的博客 417

Java集合之HashMap(1.7和1.8)源码分析

Java集合之HashMap 本文将从以下几个方面来理解HashMap: 什么是哈希表 HashMap源码分析 HashMap和HashTable的区别 1.7和1.8有什么区别 哈希表 哈希表是种数据结构,它可以提供快速的插入操作和查找操作。第一次接触哈希表时。不论哈希表中有多少数据,插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。 为什么哈希这么快呢 首先不得不提哈希函数了( p= H(key) ),哈希函数就是给一个任意大小的数据生成出一个固定长度的数据,作为它的映射 ,这种

全是bug写个蛇的博客 333

7.Java SDK源码分析系列笔记-JDK1.8 HashMap

1.1.Java SDK源码分析系列笔记-Integer2.2.Java SDK源码分析系列笔记-String系列3.3.Java SDK源码分析系列笔记-LinkedList4.4.Java SDK源码分析系列笔记-ArrayList5.5.Java SDK源码分析系列笔记-Vector6.6.Java SDK源码分析系列笔记-SynchronizedList7.7.Java SDK源码分析系列笔记-JDK1.8 HashMap8.

weixin_40316402的博客 94

HashMap工作原理回顾 (基于JDK1.8源码分析)

HashMap工作原理回顾 (基于JDK1.8源码分析) 空的HashMap()构造方法 /** * 默认初始容量为16. */ static final int DEFAULT_INITIAL_CAPACITY = 1 &amp;amp;lt;&amp;amp;lt; 4; // aka 16 /** * 最大容量为2的30次方,如果有参构造器指定了更大的数值,那么仍...

wendrewshay 793

new hashmap 初始大小_二、Jdk1.7和1.8中HashMap数据结构及源码分析

导读上篇文章(一、深入理解-Java集合初篇 )中我们对Java的集合体系进行一个简单的分析,本篇我们我们将针对Java集合体系中的HashMap做详细介绍。简单介绍JDK1.7---》哈希表,链表JDK1.8---》哈希表,链表,红黑树--- JDK1.8之后,当链表长度超过8使用红黑树。非线程安全0.75的负载因子,扩容必须为原来的两倍。默认大小为16,传入的初始大小必须为2的幂次方的值,如果...

weixin_39653733的博客 383
上一篇: Java简单计算器(栈实现)
Java-fly
博客等级 码龄9年 13粉丝 31原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值