HashMap

HashMap

1. 什么是哈希表

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,在探讨哈希表的性能前,我们先大概了解一下其他数据结构的增删查改的性能

**数组:**采用一段连续的存储单元来存储数据,对指定的下标进行查找,时间复杂度为O(1);通过指定的值在数组中查找,则需要遍历数组,时间复杂度为O(n);删除、插入的操作,则涉及到数组元素的移动,时间复杂度为0(n);

**线性链表:**跟数组相反的,链表中的删除、插入操作只需要处理结点引用即可,时间复杂度为O(1),而查找操作需要的时间复杂度为O(n)

**二叉树:**对一颗平衡的有序二叉树,对其进行增删查改等操作,其平均复杂度为O(logn)

**哈希表:**相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。

我们知道,数据结构的物理存储结构只有两种:顺序存储结构链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组

比如我们要新增或查找某个元素,我们通过把当前元素的哈希值 映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
  
这个函数可以简单描述为:存储位置 = f(关键字) ,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:
插入过程如下图所示

哈希冲突

如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,HashMap即是采用了链地址法,也就是数组+链表的方式,但是如果同一哈希值的元素都存储在同一个链表上,即哈希值相等的元素过多的时候,会造成查找效率降低,所以在JDK8中采用了数组+yi链表+红黑树的方式,但是一个链表的长度超过**阈值(8)**的时候,将链表转换为红黑树,这样减少了查找时间,性能会更好。

2. HashMap的hash算法

这是HashMap的hash算法,通过这个方法计算出的哈希值决定将元素存储在数组的什么位置上

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

乍看一下就是简单的异或运算和右移运算,但是为什么要异或呢?为什么要移位呢?而且移位16?

在分析这个问题之前,我们需要先看看另一个事情,什么呢?就是 HashMap 如何根据 hash 值找到数组种的对象,我们看看 get 方法的代码:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

我们可以看到 get 方法就是将传进来的 key 用 hash 方法计算出它的哈希值,然后调用 getNode 方法,在 getNode 方法中,我们可以看到这一行代码:

first = tab[(n - 1) & hash]

使用数组的长度减一和 key 的哈希值进行与运算,这行代码就是为什么要让前面的 hash 方法移位并异或。我们分析一下:

首先,假设有一种情况

对象 A 的 hashCode 为 1000010 00111000 10000011 11000000,

对象 B 的 hashCode 为 0111011 10011100 01010000 10100000。

如果数组长度是16,也就是 15 与运算这两个数, 你会发现结果都是 0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。

但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免我们上面的情况的发生。

总的来说,使用位移 16 位和 异或 就是防止这种极端情况。但是,该方法在一些极端情况下还是有问题,比如:10000000000000000000000000 和 1000000000100000000000000 这两个数,如果数组长度是16,那么即使右移16位,在异或,hash 值还是会重复。但是为了性能,对这种极端情况,JDK 的作者选择了性能。毕竟这是少数情况,为了这种情况去增加 hash 时间,性价比不高。

3. HashMap 为什么使用 & 与运算代替模运算?

知道了 hash 算法的实现原理还有他的一些取舍,我们再看看刚刚说的那个根据hash计算下标的方法:

tab[(n - 1) & hash]

其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。但是,对于现代的处理器来说,除法和求余数(模运算)是最慢的动作。

上面情况下和模运算相同呢?

a % b == (b-1) & a ,当b是2的幂次方时,等式成立。

我们说 & 与运算的定义:与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0;

当 n 为 16 时, 与运算 101010100101001001101 时,也就是
1111 & 101010100101001001000 结果:1000 = 8
1111 & 101000101101001001001 结果:1001 = 9
1111 & 101010101101101001010 结果: 1010 = 10
1111 & 101100100111001101100 结果: 1100 = 12

可以看到,当 n 为 2 的幂次方的时候,减一之后就会得到 1111 的数字,这个数字正好可以掩码。并且得到的结果取决于 hash 值。因为 hash 值是1,那么最终的结果也是1 ,hash 值是0,最终的结果也是0。

4. HashMap 的容量为什么建议是 2的幂次方?

到这里,我们提了一个关键的问题: HashMap 的容量为什么建议是 2的幂次方?正好可以和上面的话题接上

为什么要 2 的幂次方呢?

我们说,hash 算法的目的是为了让hash值均匀的分布在桶中(数组),那么,如何做到呢?试想一下,如果不使用 2 的幂次方作为数组的长度会怎么样?

假设我们的数组长度是10,还是上面的公式:
1010 & 101010100101001001000 结果:1000 = 8
1010 & 101000101101001001001 结果:1000 = 8
1010 & 101010101101101001010 结果: 1010 = 10
1010 & 101100100111001101100 结果: 1000 = 8

看到结果我们惊呆了,这种散列结果,会导致这些不同的key值全部进入到相同的插槽中,形成链表,性能急剧下降。

所以说,我们一定要保证 & 中的二进制位全为 1,才能最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。如果是 1010,有的散列结果是永远都不会出现的,比如 0111,0101,1111,1110…….,只要 & 之前的数有 0, 对应的 1 肯定就不会出现(因为只有都是1才会为1)。大大限制了散列的范围。

5. HashMap 构造函数

Map<String, Integer> map = new HashMap<>(2);
map.put("one", 1);
Integer one = map.get("one");
System.out.println(one);

一个简单的使用 HashMap 的例子,其中包含了 HashMap 的三个关键步骤:初始化、put、get;

我们先来看 new 操作的时候发生了什么:

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);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

上面是 HashMap 的两个构造方法,其中,我们设置了初始容量为 2, 而默认的加载因子我们之前说过:0.75,当然也可以自己设置,但 0.75 是最均衡的设置,没有特殊要求不要修改该值,加载因子过小,理论上能减少 hash 冲突,但是会频繁的扩容数组增加 HashMap 中的最耗性能的操作:reHash,而加载因子过大可以节约空间减少 reHash 操作,但是会导致 hash 冲突频繁

从代码中我可以看到,如果我们设置的初始化容量小于0,将会抛出异常,如果加载因子小于0也会抛出异常。同时,如果初始容量大于最大容量,则重新设置为最大容量。

我们开最后两行代码,首先,对负载因子进行赋值,这个没什么可说的。
牛逼的是下面一行代码:this.threshold = tableSizeFor(initialCapacity); 可以看的出来这个动作是计算阀值,什么是阈值呢?阀值就是,如果容器中的元素大于阀值了,就需要进行扩容,那么这里的这行代码,就是根据初始容量进行阀值的计算。

我们进入到该方法查看:

static final int tableSizeFor(int cap) {
    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;
}

一通的或运算和无符号右移运算,那么这个运算的的最后结果是什么呢?这里其实就是如果用户输入的值不是2的幂次方(我们通过之前的分析,应该直到初始容量如果不是2的幂次方会有多么不好的结果,增加了哈希冲突)。通过位移运算和或运算,最后得到一定是2的幂次方,并且是那个离那个数最近的数字,我们仔细看看该方法:

首先将容量减1,然后将该数字无符号的右移1,2,4,8,16,即总共移动了32位,在移动的过程中,还对该数字进行或运算,为了方便查看,写一下这个方法的运算过程,假如我输入的是10,明显不是2的幂次方。我们看看会怎么样:

10 = 1010;
n = 9 = 1001;
1001 >>> 1 = 0100;
1001 | 0100 = 1101;
1101 >>> 2 = 0011;
1101 | 0011 = 1111;
1111 >>> 4 = 0000;
1111 | 0000 = 1111;
1111 >>> 8 = 0000;
1111 | 0000 = 1111;
1111 >>> 16 = 0000;
1111 | 0000 = 1111;

最后得到 1111,也就是15,然后判断是否大于最大的容量,如果大于则返回最大容量,否则返回算出来的结果加一,也就是 15 + 1,得到16,刚好就是距离10最近且没有变小的2的幂次数。

如果本来就是一个2的幂次数容量呢,会有什么结果:

16 = 10000;
n = 15 = 1111;
1111 >>> 1 = 0111;
1111 | 0111 = 1111;
...
1111 | 1111 = 1111;

结果会得到一个它本身,但是如果我们不进行先减1的操作就是进右移和或运算的话,就会得到一个上升到更大的2次幂的数,但是这不是我想要的结果,所以,JDK 的作者在之前先减去了1,防止出现这样的问题。

我们仔细观察其算法的过程,可以说,任何一个int 数字,都能找到离他最近的 2 的幂次方数字(并且比他大)。

好了。到这里就完成了初始化,不过请注意,这里设置的阀值并不是最终的阀值,最终的阀值我们会在后面详细说明。这里我们更加关注这个算法。真的牛逼啊。

6. HashMap put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断数组是否为null,或者长度为0;第一次调用put方法都会进行里面的resize()方法创建一个数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // hash值跟数组的长度-1进行与运算获取数组下标,之前介绍过
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 为空的话就新建一个Node放入数组
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果获取的节点跟hash值一样和 key 一样或者equals一样的话,就找到了这个节点
        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) {
                // 如果这个节点的下个节点为空,就增加在尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 当链表的长度大于等于8就转换为红黑树
                    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;
            }
        }
        // 当e不为null说明这个key已经存在了,如果onlyIfAbsent为false或者原来的value为空就替换为新传进来的value,然后返回旧的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 增加修改次数,如果在调用foreach遍历的时候,调用了put,get方法会使modCount发生变化,从而抛出并发修改错误;
    ++modCount;
    // 如果map的容量大于阈值,就resize()进行扩容和重新散列hash
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

这个方法是 HashMap 的核心方法,在方法中已经写满了注释,该方法的步骤为:

  1. 判断数组是否为空,如果为空,就创建一个数组,第一次调用 put 方法的时候,都会创建一个数组
  2. 通过与运算计算出对应哈希值的下标,如果对应下标的位置没有元素,则直接创建一个元素
  3. 如果有元素,就说明发生了哈希冲突,则进行三种判断:
    1. 判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新e的value,也就是替换操作。
    2. 如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。
    3. 如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。
  4. 最后如果这三个判断返回的 e 不为 null 的话,就说明已经存在这个 key ,更新对应 key 的 value。
  5. 对维护着迭代器的 modCount 变量加一
  6. 最后判断当前的容量是否大于阈值,大于则进行扩容和 rehash

7. HashMap 重新散列方法

上面我们说到如果 map 的容量大于阈值的话,就进行扩容和重新散列 hash,而 resize() 方法就是重新散列的核心方法,该方法的实现:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果老的容量大于0
    if (oldCap > 0) {
        // 如果大于最大容量,就将阈值设置为int的最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 不然就将老的容量扩容两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 如果新的容量小于最大的容量和老的容量大于默认的容量(16),就将老的阈值乘以2为新的阈值
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新的阈值为0
    if (newThr == 0) {
        // 新的阈值就等于新的容量乘以负载因子(0.75)
        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字段
    table = newTab;
    // 如果老的数组不为 null
    if (oldTab != null) {
        // 循环遍历老的数组
        for (int j = 0; j < oldCap; ++j) {
            // 定义一个节点
            Node<K,V> e;
            // 如果老数组对应的下标不为空,将对应下标的值赋值给 e
            if ((e = oldTab[j]) != null) {
          		// 设置为空
                oldTab[j] = null;
                // 如果 e 只有一个元素
                if (e.next == null)
                    // 将该值散列到新的数组中
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果该节点为树
                else if (e instanceof TreeNode)
                    // 调用红黑树 的split 方法,传入当前对象,新数组,当前下标,老数组的容量,目的是将树的数据重新散列到数组中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 如果 next 节点不为空,也不为红黑树,那就是链表了
                else { // preserve order
                    // loHead lo头部 loTail lo尾部 
                    // hiHead hi头部 hiTail hi尾部
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 如果该值的哈希值跟老容量进行与运算结果为0时,说明该元素的下标位置不变,后面会详解
                        if ((e.hash & oldCap) == 0) {
                            // 第一次进来给链头赋值,这部分很像将一个节点插入链表所需要做的操作
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            // 同lo
                            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;
}

该方法可以说还是比较复杂的。初始的时候也是调用的这个方法,当链表数超过8的时候同时数组长度小于64的时候也是调用的这个方法。该方法步骤如下:

  1. 判断容量是否大于0,如果大于0,并且容量已将大于最大值,则设置阀值为 int 最大值,并返回,如果老的容量乘以 2 小于最大容量,且老的容量大于等于16,则更新阀值。也就是乘以2.
  2. 如果老的容量为0和老的阀值大于0,则新的容量等于老的阀值。注意:这里很重要。还记的我们之前使用new 操作符的时候,会设置阀值为 2 的幂次方,那么这里就用上了那个计算出来的数字,也就是说,就算我们设置的不是2的幂次方,HashMap 也会自动将你的容量设置为2的幂次方。
  3. 如果老的阀值和容量都不大于0,则认为是一个新的数组,默认初始容量为16,阀值为 16 * 0.75f,也就是 12。
  4. 如果,新的阀值还是0,那么就使用我们刚刚设置的容量(HashMap 帮我们算的),通过乘以 0.75,得到一个阀值,然后判断算出的阀值是否合法:如果容量小于最大容量并且阀值小于最大容量,那么则使用该阀值,否则使用 int 最大值。
  5. 将刚刚的阀值设置打当前Map实例的阀值属性中。
  6. 将刚刚的数组设置到当前Map实例的数组属性中。
  7. 如果老的数组不是null,则将老数组中的值重新散列到新数组中。如果是null,直接返回新数组。

其中重新散列的过程是最难的,也是最核心的:

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);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

首先定义了五个变量,其中四个看名字可以大概知道两个为一组的,一组为 higher 位置的链表,一组为 low 位置的链表,那么这里的 higher 和 low 位置是什么意思呢,还有为什么重新散列的时候是用 (e.hash & oldCap) == 0 进行判断呢?

首先我们要明确 HashMap 的容量一定是2的整数次幂

假设当前容量是默认的16
(容量 - 1) & hash 是put方法里面确定插入元素属于哪个数组下标
16 -1 = 15 的二进制为:0000 0000 0000 0000 0000 0000 0000 1111
这个时候与hash值进行"&"计算,算出相应数组下标,决定因素在后四位

16 扩容后 是 32
32-1 = 31 的二进制为:0000 0000 0000 0000 0000 0000 0001 1111
这个时候与hash值进行"&"计算,计算相应数组下标,决定因素在后五位

二者区别就在第五位上

假设现在有一个插入元素的key的hash值二进制第五位是0,进行相应计算,最后计算结果二者是一样的。

也就是说假如咱们一个一个的去计算元素所应该在什么位置时,当元素的key的hash值第五位是0 这个时候放置的位置与之前未扩容之前是一样的。

再次假设现在有一个插入元素的key的hash值二进制第五位是1,进行相应计算,最后计算结果二者相差16,也就是说第一个计算出来的数组下标加上16就是第二计算出来的数组下标。

也就是说,我们可以进行一次判断,判断元素的hash值第五位是0还是1,0则不变还是之前的数组下标即可,1的话就是原本的数组下标加上16就是在新数组的新下标

只需要看第五位其余位置无所谓,那么就让其他位置为0,第五位为1(这不就是扩容前容量16吗),然后与元素hash值做与运算
这不就是咱们所看的这个判断条件吗 (e.hash & oldCap) == 0

所以在两个if判断那里,将两个链表一个放到 j (low 位置)一个放到 j+oldCap (heigh 位置)

(head 和 tail 操作图):

h值二进制第五位是0,进行相应计算,最后计算结果二者是一样的。

也就是说假如咱们一个一个的去计算元素所应该在什么位置时,当元素的key的hash值第五位是0 这个时候放置的位置与之前未扩容之前是一样的。

再次假设现在有一个插入元素的key的hash值二进制第五位是1,进行相应计算,最后计算结果二者相差16,也就是说第一个计算出来的数组下标加上16就是第二计算出来的数组下标。

也就是说,我们可以进行一次判断,判断元素的hash值第五位是0还是1,0则不变还是之前的数组下标即可,1的话就是原本的数组下标加上16就是在新数组的新下标

只需要看第五位其余位置无所谓,那么就让其他位置为0,第五位为1(这不就是扩容前容量16吗),然后与元素hash值做与运算
这不就是咱们所看的这个判断条件吗 (e.hash & oldCap) == 0

所以在两个if判断那里,将两个链表一个放到 j (low 位置)一个放到 j+oldCap (heigh 位置)

(head 和 tail 操作图):

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值