HashMap 底层原理探究

1 篇文章 0 订阅
本文深入剖析了HashMap的底层实现,包括其基于Hash表的数据结构,存取机制中的初始化、put元素过程,以及resize()方法的扩容策略。详细解释了tableSizeFor()方法如何确保数组长度为2的幂次方,put操作中hashCode()的处理,以及为什么长度必须是2的幂。此外,还探讨了get元素的查找逻辑。
摘要由CSDN通过智能技术生成

前言:

  hashMap 是一个 key——value 映射的集合,它的数据结构由 hash 表来实现,可以通过 key 达到快速获取 value 的效果。

  本文将基于 JDK8 来分析 HashMap 底层实现原理。

1. Hash 表的实现

  hash 表是由数组 + 链表实现了,至于数组和链表的优缺点,这里不做概述。

在这里插入图片描述

  当需要通过 hash 表查询某一 key 值的 value。比如:对 key8 进行 hash 计算,得出数组的下标是 1,再基于链表遍历形式比较 key8,获取 key8 的 value8 即可。如果当链表太长,我们也可以将其转换为红黑树。

2. HashMap 的实现

2.1 数据结构

  HashMap 就是基于 Hash 表实现的,它的每一个元素是个 Node<K,V> 对象,里面有 key、value、hash 等成员变量。Node 类如下所示:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // key 计算后的 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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
  
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

  Node 实现了 Map.Entry 接口,它必须实现 getkey()、getValue()、hashCode()等方法。

  HashMap 使用数组 table 来存放 Node 元素,如果出现 node 的 hash 值一样,key 值不一样,则会以链表的形式往下添加。

transient Node<K,V>[] table;

2.2 存取机制

2.2.1 初始化

  执行如下代码,创建 HashMap 对象

HashMap<String, Integer> map = new HashMap<>();

  调用 HashMap 无参构造函数

public HashMap() {
    // 扩容因子 float DEFAULT_LOAD_FACTOR = 0.75f;
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

  这里可以看到,简单创建一个 HashMap 是不会设置对 table 数组进行初始化的,只是简单设置下扩容因子,后续 put 元素会调用 resize() 方法进行扩容。

  当然我们也可以手动设置 table 数组大小。

 HashMap<String, Integer> map = new HashMap<>(7)
// 可以调用有参构造手动设置 table 数组大小
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

  initialCapacity 值会经过 tableSizeFor() 转换得到真正的 table 数组初始化长度。

	/**
     * 根据容量参数,返回一个2的n次幂的table长度
     */
    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;
    }

  这个方法会返回 大于等于 cap 的 2 的 n 次方值,作为 table 数组长度。这里可以看出,即便我们设置初始化长度最后也会转换为一个 大于等于 cap 的 2 的 n 次方值作为 tableSize。这里抛出一个问题,tableSize 为什么要返回一个 2 的 n 次方作为数组长度?这个问题,在本文后面会进行详细解答。

tableSizeFor 方法解析

  该方法设计的非常巧妙,但是又很晦涩难懂,看不懂的同学可以直接跳过。

  1. 首先我们必须知道该方法会返回一个大于等于 cap 的 2 的 n 次方数。那在 32 位bit下 2 的 n 次方数有哪些?

2^0:0000 0000 0000 0000 0000 0000 0000 0001   2^0 -1:0000 0000 0000 0000 0000 0000 0000 0000

2^1:0000 0000 0000 0000 0000 0000 0000 0010   2^1 -1:0000 0000 0000 0000 0000 0000 0000 0001

2^2:0000 0000 0000 0000 0000 0000 0000 0100   2^2 -1:0000 0000 0000 0000 0000 0000 0000 0011

2^3:0000 0000 0000 0000 0000 0000 0000 1000   2^3 -1:0000 0000 0000 0000 0000 0000 0000 0111

… …

2^6:0000 0000 0000 0000 0000 0000 0100 0000   2^6 -1:0000 0000 0000 0000 0000 0000 0011 1111

… …

2^31:0100 0000 0000 0000 0000 0000 0000 0000   2^31 -1:0011 1111 1111 1111 1111 1111 1111 1111

  这个也就是 MAXIMUM_CAPACITY 的值

2^32:1000 0000 0000 0000 0000 0000 0000 0000   2^32 -1:0111 1111 1111 1111 1111 1111 1111 1111

  这个已经超过 int 的最大取值范围了

  从上面看,2 的 n 次方数的二进制形式是不是很有规律。现在要你实现一个算法,就是在 int 取值范围中的任何二进制数要转换为一个大于等于且最接近的 2 的 n 次方数,你会怎么实现?

举例子:

  数值74: 0000 0000 0000 0000 0000 0000 0100 1010

  经过算法要转化为

  数值2^7:0000 0000 0000 0000 0000 0000 1000 0000

  这个看起来很简单,直接最高位1往前进一位,最高位后面都置于0就可以。但通过高效的按位运算和位移运算做起来很难。tableSizeFor() 方法是经过这样转化的。

将要转化为 2^n 改为 要转化为 2^n - 1次方。因为 2^n - 1 有效位都是1,更好使用按位运算。

  数值74 - 1: 0000 0000 0000 0000 0000 0000 0100 1001

  经过算法要转化为

  数值2^7 - 1:0000 0000 0000 0000 0000 0000 0111 1111

  最终将得出的 2^7 - 1 + 1 就是 2^7。这个看起来也简单,只要将最高位 1 以后的数都置为 1 即可。

再来看 tableSizeFor() 方法是怎么做的,首先执行:

int n = cap - 1;

  也就是 n 要转化最终结果要是 2^N - 1,最后返回的时候 + 1即可。接下来执行位移运算 >>> 和 位运算 | :

| 运算如下:

   1 0 0
   | | |
   1 1 0
........1 1 0       

  这样里为了好看,稍微改了下代码,本质不变。

n = n | (n >>> 1);  
n = n | (n >>> 2);
n = n | (n >>> 4);
n = n | (n >>> 8);
n = n | (n >>> 16);

  如果 n 是 0,最后转化的完 n 也是 0,这种情况不考虑。我们考虑 n != 0 的情况。如果 n != 0 ,最高位肯定是 1。

  1. 步骤1,先将最高位 1 往右无符号位移 1 个位置,我不需要判断最高位在哪,我只需要最高位往右无符号位移 1 个位置,然后 n | (n >>> 1),就可以让 n 的最高位和次高位都是 1,然后赋值给 n。这时的n,最高位及最高位往后 1 位都是 1。
  2. 步骤2,执行 (n >>> 2),将 n 最高位和次高位无符号往后移 2 个位置位,然后 n | (n >>> 2),因为最高位和次高位都是1,往后移 2 位,也就是 (n >>> 2) 得到二进制,最高位往后数 3、4位也是 1,再执行 n | (n >>> 2) ,然后赋值给 n,这时的 n 最高位及最高位往后数 3 位都是 1。
  3. 执行完步骤3,得到的 n,最高位及最高位往后数 7 位都是 1。
  4. 整个都执行完,得到 n,最高位及最高位往后数 31 位都是 1。

  也就是个算法执行完,它可以保证 n 的二进制最高位后面所有位都转化为 1。

比如:数值74 - 1: 0000 0000 0000 0000 0000 0000 0100 1001 ,经过转化后,会得到

数值2^7 - 1:0000 0000 0000 0000 0000 0000 0111 1111

  最后再执行两个三元表达式:

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

  如果 n < 0 返回 2^0,如果 n >= MAXIMUM_CAPACITY(2^31),则返回 2^31,其他则返回 n + 1。也就是输入数值 74 最后会返回 2^7 - 1 + 1。

2.2.2 put 元素

  首先会调用这个方法:

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

再 put 元素之前,先对 key 进行 hash 计算,得到一个 hash值。

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

  重点看 hash = (h = key.hashCode() ) ^ ( h >>> 16 )代码。

为什么要重写 hashCode()?

  如果 k 是个对象,则调用对象本身的 hashCod() 方法,如果没重写,则调用 Object类的 hashCode 方法;如果是包装类,都有重写 hashCod() 方法。比如 String key1 = “a” 和 String key2 = “a”,这两个 key 调用 String.hashCode() 方法后 得到的 hash 值是一样的,这也符合我们的意愿,相同的字符串 hash值是一样的。但如果是 Object 类,如以下代码:

Object key1 = new Object();
Object key2 = new Object();
System.out.println(key1.hashCode());
System.out.println(key2.hashCode());

  执行完得到的 hash值是不一样的,因为它们本身就是两个不同对象。但如果我们把自定义类作为 key 时候,就得好好考虑下,该不该重写 hashCode() 方法了。

  比如自定义 Key 类:

public class Key {
    
    private String st1;

    public Key(String st1) {
        this.st1 = st1;
    }
    
}

  执行如下代码,你会发现,这虽然是两个不同的 Key对象,但是有相同的成员变量 st1,应该要有相同的 Hash值,所以你必须重写 hashCode() 方法。

Key key1 = new Key("a");
Key key2 = new Key("a");
System.out.println(key1.hashCode());
System.out.println(key2.hashCode());

  重写 hashCode() 方法,再执行如上代码,就得到相同的 hash值了

public class Key {
    
    private String st1;

    public Key(String st1) {
        this.st1 = st1;
    }
    
    @Override
    public int hashCode() {
        return st1.hashCode();
    }
}

  再看hash = (h = key.hashCode() ) ^ ( h >>> 16 )

为什么要执行 h ^ (h >>> 16) ?

  在计算出 h 值后,h = key.hashCode()。会执行 h ^ (h >>> 16),这个步骤是为了干嘛?

  将 h无符号右移 16 为相当于将高区 16位移动到了低区的 16位,再与原 h 做异或运算,可以将高低位二进制特征混合起来,也就是低 16位 集成了高 16位和低 16位本身的特征。为什么要这么做呢?

  在计算元素应该存在 table 数组哪个下标的时候,是通过 (table.length - 1) & hash 来计算出下标。

& 运算如下:

   1 0 0
   & & &
   1 1 0
........1 0 0       

  我们可以知道,当 length 很大的时候,超过 16 位,hash 的高位也会参与计算,这没问题;但如果 length 很小的时候,只有低 16位,高位全是 0,那么 hash 只有低位会参与计算,高位的特征就失效了。hash值有多少位参与运算完全是跟 length 有关,当 length 太小,2个不同的 hash 值只要 length 有效位是一样的就会得出同样的下标。

举例子:当 length = 8 的时候,8 - 1,二进制:0000 0000 0000 0000 0000 0000 0000 0111

  现有 2个不经过 h ^ (h >>> 16)运算的 hash值:

hash1:0000 0000  0000 0010  0000 0000 0000 0101  

hash2:0000 0000  0000 0000  0000 0000 0000 0101  

  它们在计算下标后,都是 5。

  如果它们都执行 h ^ (h >>> 16)后,再去计算下标,得出一个是 5,一个是 7。

  所以该代码目的,在 length 比较小的时候,hash 值低位也集成高位的特征,使得计算出的下标更加分散,减少重复,也就是减少 hash碰撞。

为什么 length 长度要是 2的幂次方

  旨在高效计算减少hash碰撞,怎么理解?

  • 假如 table.length 可以是任意长度,比如 5,5 - 1的二进制是:

0000 0000 0000 0010 0000 0000 0000 0100,那它在 & hash的时候你会发现,计算的下标全是4,其他下标根本计算不出来。

  • 这里你肯定会想,我用 hash % (table.length - 1),也能很好的计算出下标,但是它没有上面表达式高效。我们在调用 get(key) 的时候也会经过计算下标,所以效率问题还是需要考虑进去。
putVal 方法详解

  计算出了 key 的 hash值后,我们继续往下分析源码:

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)
            // 如果 tab = null 或者 tab.length = 0,则进行扩容
            n = (tab = resize()).length;
    
     // 计算出索引下标位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 如果tab数组索引下标上没有 node,则创建一个 node,存入 hash、key、value等值
            tab[i] = newNode(hash, key, value, null);
        else {
            
            /**
            如果tab数组下标上有 node,则说明发生了 hash碰撞,
            这里会比较 tab数组下标的 node与新增的 node是否判定为同一个进行覆盖,不是同一个,进行链表添加进去。   
            */
            
            Node<K, V> e;
            K k;
            
            /**
              根据 hash值是否一样 && (key对象是否是同一个 || 调用equals方法后返回true) 来判断是覆盖还是进行链表添加
             */
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
              
                // 需要进行覆盖,进入这里,后续会替换value的值
                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) {
                    // 这里如果没发生 node需要覆盖,则一直遍历下去,直到新node添加到尾节点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 这里判断 链表长度是否大于等于 8
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            // 链表转换为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    // node 节点遍历时,需要判断是否有节点 和新增节点是同一个进行覆盖
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            
            if (e != null) { // existing mapping for key
             // e != null 说明需要覆盖,修改value的值   
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
    
    	/**
    		判断是否需要扩容	
    		threshold = tab.length * 扩容因子(默认是0.75)
    		size 是map中所有node的个数
    	*/
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  当 hash 计算出的下标上有元素 p 时,我们需要根据如下校验来判断是否进行覆盖还是新增。

(p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
  1. 首先计算出新元素在数组中索引下表的位置
  2. 如果该索引下表位置没有 node,则生成新的 node添加进去,如果有 node,则会进行判断
  3. 如果两个 key 的 hash值不一样,只是它们计算出的索引下标是一样,认为它们是不同的 key
  4. 如果 hash值一样,则会可能发生 hash碰撞,需要继续判断
  5. 通过 == 直接判断 key 是不是同一对象
  6. 不是同一个对象,则调用 key 对象自定义重写的 equals() 方法来判断是否是同一个 key。

  是同一个key,则进行覆盖,遍历结束,都不是同一key,则基于链表 or 红黑树添加下去。

  1. 如果 node 元素个数 > table.length * 扩容因子(0.75),则会进行扩容。

该方法逻辑图如下所示:

在这里插入图片描述

2.2.3 resize 方法详解

  resize 方法主要是用来对 table 数组扩容的,为什么要扩容?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低。加载因子(默认0.75),当达到这个比例,则会进行扩容。

final Node<K,V>[] resize() {
    
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
    
        if (oldCap > 0) {
            // 旧表的长度不为0,进入这里
            
            // 判断旧表长度是否大于允许最大长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            // 新表长度newCap = 旧表长度oldCap * 2    
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                
                newThr = oldThr << 1; // double threshold
            
        }else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
    
        else {
            // 旧表长度是0,无参构造刚创建的HashMap,并没有初始化table数组,进入这里
            // newCap = 16。初始化长度为16
            // newThr = 0.75 * 16 = 12。当前table扩容阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    
        if (newThr == 0) {
            // 如果新表扩容阈值 = 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 = newTab;
        if (oldTab != null) {
            // 旧表不为null,需要将旧表数据复制到新表
            for (int j = 0; j < oldCap; ++j) {
                // 遍历旧表
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // oldTab[j] != null
                    
                    oldTab[j] = null;
                    if (e.next == null)
                        // 如果oldTab[j] 只有一个 node,直接添加到新表
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        // 如果oldTab[j]上 不止一个 node,并且是TreeNode 
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                    	// preserve order
                    	/*
                    	 * 当节点上的数据太多,需要使用这种方式快速转移数据到新表
                    	 */
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            
                            /**
                              判断node节点的索引下标是否需要修改
                            */
                            if ((e.hash & oldCap) == 0) {
                                // 索引下标不需要改变的 node组成链表
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            } else {
                                // 索引下标需要改变的 node组成链表
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        if (loTail != null) {
                            // 索引不变的 node链表,将头节点添加进新表 原位置j
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            // 索引需要改变的 nide链表,将头节点添加进新表 
                            // 原位置j + oldCap 组成新的索引下标					
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
    	// 返回新表
        return newTab;
    }
(e.hash & oldCap) 算法

  (e.hash & oldCap)这个代码很巧妙,我们知道 oldCap 是 2的n次方。这里假设 oldCap = 16,newCap = 16 * 2。

oldCap 	   : 0000 0000  0000 0000  0000 0000 0001 0000
oldCap - 1 : 0000 0000  0000 0000  0000 0000 0000 1111
newCap 	   : 0000 0000  0000 0000  0000 0000 0010 0000
newCap - 1 : 0000 0000  0000 0000  0000 0000 0001 1111

  可以看出,newCap 比 oldCap 高一位,newCap - 1 比 oldCap - 1 也高一位。如果(e.hash & 16) = 0,说明 e.hash 在 newCap - 1 最高位肯定是0,所以 e.hash & oldCap - 1 == e.hash & newCap - 1,索引下标位置不需要变:

举例子:(e.hash & 16) = 0,e.hash 二进制如下:

XXXX XXXX XXXX XXXX XXXX XXX XXX0 XXXX。X 可以是 0 or 1;

  旧表索引下标

XXXX XXXX  XXXX XXXX  XXXX XXXx XXX0 XXXX
&
0000 0000  0000 0000  0000 0000 0000 1111 oldCap - 1 : 15
=
0000 0000  0000 0000  0000 0000 0000 XXXX

  新表索引下标计算

XXXX XXXX  XXXX XXXX  XXXX XXXx XXX0 XXXX
&
0000 0000  0000 0000  0000 0000 0001 1111 newCap - 1 : 31
=
0000 0000  0000 0000  0000 0000 0000 XXXX

  得出的索引下标是一样的。

  这里还有一个问题,索引下标需要修改的 node 它在新表中的位置可以直接通过 原位置 + oldCap 确定,也就是,e.hash & (oldCap - 1) +oldCap == e.hash & (oldCap <<<1 - 1)?

  其实是一样的,这里不做过多推导。

总结扩容步骤

  1. 判断 table 是否需要新初始化,初始化长度默认是16,如果不是,新表长度 = 旧表长度 * 2
  2. 遍历旧表所有索引下标上的 node
  3. 如果 node 不为空,且没有 next节点,则直接进行添加到新表
  4. 如果 node 不为空,node 是红黑树节点。。。。。
  5. 如果 node 不为空,是链表的头节点,这里会判断分成2个链表,一个是需要改变索引下表的链表,另外一个是不需要改变索引下表的链表,最后都添加到新表
  6. 返回新表
2.2.4 get 元素

  通过get方法,我们可以基于 key 拿到 value

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

  key 进行hash计算

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) {
            // 通过 (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;
    }

  我们存是以 key.hash & (length - 1) 计算出索引下标位置的,我们查寻找出目标元素的索引下标位置也要基于这个方式计算。同时在判断 node 是不是我们要找的那个,判断逻辑和存也是一样的,都是基于((k = first.key) == key || (key != null && key.equals(k))))。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值