• HashMap学习


    HashMap知识整理

    最近在看jdk8之后的hashmap的源码(主要是引入红黑树),写下博客记录学习。
    具体看Gitee地址:https://gitee.com/dz138598/hash-map/tree/master

    Hash定义

    把任意长度的输入,通过一个hash算法,映射成固定的长度的输出。

    Hash冲突

    定义

    当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象。即两个对象调用hasCode方法计算得到的hash相同。

    hash冲突在理论上是没有办法避免的,多映射到少的时候那肯定会存在一个冲突的问题。也就是我们常说的鸽笼原理。

    解决的几种办法

    1. 开放定址法
      • 线性探查法
      • 平方探查法
      • 双散列探查法
    2. 链地址法
    3. 再哈希法
    4. 建立公共溢出区

    具体分析

    线行探查法:
    最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。
    平方探查法
    发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等,直到找到空闲单元。平方探查法可能不能探查到全部剩余的单元。
    
    链接地址法
    将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。
    
    再哈希法
    就是同时构造多个不同的哈希函数,第一个哈希函数当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
    
    建立公共溢出区
    将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。
    

    HashMap

    存储规则说明

    1. HashMap<String,Object> map = new HashMap();

      创建HashMap对象后,并没有在创建集合对象的创建数组,而是首次调用put方法时,底层创建长度是16的Node[] table数组。

    2. 假设向哈希表存储数据name:zhangsan,根据name,调用String类的hasCode()方法计算出哈希值,此哈希值经过某种算法计算之后,得到在Node数组中存放的位置(是2),如果此位置是空,就直接添加到该位置。

    HashMap中hash函数是如何实现的?

    对key的hashCode做hash操作,无符号右移16位做异或运算。

    下标计算方式:hashCode%length,而计算机中求余的效率不如位运算。而当length是2的n次幂,hashCode()%length等价于hashCode()&(length-1)

    1. 假设向哈希表又中存储了sex:男,根据sex,计算出hash值和通过算法得到下标(是2)。但是2的位置已经存在了name:zhangsan。如果哈希值不相同,那么sex:男会在此空间划出一个节点变为链表来存储。
    2. 假设向哈希表又中存储了grade:2020,根据grade,计算出hash值和通过算法得到下标(是2)。且hash值和sex的一样。那么继续调用equals方法,比较内容是否相等,不相等的话,划出一个节点变为链表来存储。

    当两个key的哈希值hashCode相同的时候,会怎么样?

    • 会发生哈希碰撞

    • JDK8之前使用数组+链表解决,而JDK8使用了数组+链表+红黑树解决

    • 若key值的内容部相同equals则会替换旧的value值。否则连接到链表后面。如果链表长度超过8,并且数组长度大于64就会转变红黑树存储

      • size表示HashMap中K-V的实时数量,注意不等于数组的长度

      • 阈值定义:当前已经占用数组长度的最大值

        • threshold(阈值)= capacity(容量)*loadFactor(加载因子0.75)
          
        • size>threshold就需要resize(扩容),扩容后的容量是之前的容量的2倍

        • 我们在实际开发中,如果对效率要求很高,应当尽可能避免hashmap的扩容。

    image-20210206003125472

    源码分析

    import java.uthil.HashMap

    HashMap继承关系

    public interface Map<K,V> {...
    
    public abstract class AbstractMap<K,V> implements Map<K,V> {..
    
    public class HashMap<K, V> extends AbstractMap<K, V>
            implements Map<K, V>, Cloneable, Serializable {...
            
            
    

    image-20210206005806593

    上面有一个很奇怪的现象:就是HashMap已经继承了AbstractMap,而AbstractMap类实现了Map接口,那为什么HashMap还要在实现Map接口?

    这样的写法,其实就是一个失误。

    成员属性分析

    静态值

    /*
    序列化版本号
    */
    private static final long serialVersionUID = 362498820763181265L;
    
    • 概念

      • 把对象转换为字节序列的过程称为对象的序列化
      • 把字节序列恢复为对象的过程称为对象的反序列化
    • 在序列化对象时,为保证在被反序列化时仍然具有唯一性,就需要给每个参与序列化的类发一个唯一的“身份证号码”——序列化版本号,那么这个类在后期怎么修改,它的终身代码的版本号都是这个序列化版本。如果不加,JVM给定义的默认序列化版本就会发生变化。此时的序列化版本号是JVM虚拟机自动计算出来的,此时进行反序列化,会因为版本不一致而出现错误。

    /**
    * 默认的初始容量(数组长度) ,=1<<4=16。HashMap的容量必须是2的n次幂
    */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    • 我们在定义HashMap的时候也可以去指定一个HashMap容量
    /**
     * 指定容量去初始化一个HashMap
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    

    当如果传入的参数不是2的n次幂,HashMap的tableSizeFor()会通过一系列的位移运算和或运算得到一个2的n次幂的结果。

    这个数字是离指定容量最近且改数字大于等于指定容量。

    	static final int tableSizeFor(int cap) {
            int n = cap - 1;
            /*
            >>> 表示符号位也会跟着移动,比如 -1 的最高位是1,表示是个负数,然后右移之后,最高位就是0表示当前是个正数。
            所以	-1 >>>1 = 2147483647
            >> 表示无符号右移,也就是符号位不变。那么-1 无论移动多少次都是-1
            原理就是将最高位 1 右边的所有比特位全置为 1,然后再加 1,最高位进 1,右边的比特位全变成 0,从而得出一个 2 的次幂的值
             */
            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-1 是为了防止cap已经是2的n次幂的情况。假设传入的值为8,没有进行减一的操作,那么得到的结果就是16。
    • 如果n=0,即cap=1。最后返回的结果是1(n+1)
    /**
    * 默认负载因子
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    /**
    * TreeNode临界值
    */
    static final int TREEIFY_THRESHOLD = 8;
    

    网上一种说法解释8

    • 红黑树的平均查找长度是log(n)如果长度为8,平均查找长度是log(8) = 3

    • 链表平均查找长度是 n/2,如果长度是8的情况下,8/2=4,效率低于红黑树,所以需要转换为红黑树

    • 如果链表长度小于等于6, 6/2=3.而log(6) ≈ 2.6,虽然比链表快,但是效率差距并不大

      • 而且,链表转换为红黑树也需要一定的时间,所以这时候并不会转换为红黑树
    /**
     * 链表值小于6会从红黑树转回链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    
    /**
     * 当数组长度大于这个数时才会转红黑树,否则只是扩容
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    

    变量

    /**
    * 实际存储的数组 Entry数组。jdk中称其为 hash桶
    */
    transient Node<K, V>[] table;
    
    /**
     * 实际存储的个数,这里的size是key-value的长度,而不是数组的长度
     */
    transient int size;
    
    /**
     * 临界值,与HashMap扩容相关
     * 计算方式:数组长度 * 负载因子
     * 当HashMap中元素个数超过这个值的时候
     * 就会进行扩容
     *
     * @serial
     */
    int threshold;
    
    /**
     * 负载因子,初始值=0.75,与扩容有关
     *
     * @serial
     */
    final float loadFactor;
    
    • 默认的负载因子是0.75,并且这个负载因子的作用是计算扩容阈值用的,比如说使用无参构造方法创建的hashmap对象,他默认情况下扩容阈值就是16*0.75,即12是扩容阈值(在第一次的情况下)
    • 负载因子是用来衡量HashMap满的程度,计算HashMap实时加载因子的方法是:size/capacity
    • loadFactor太大会 导致查找元素效率低,太小会导致数组利用率低
    • 当HashMap中容纳的元素超过边界值,认为HashMap太挤了,需要扩容。扩容的过程涉及到rehash、复制等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap时指定初始化容量来避免
      • 比如:我们需要存储1000个元素到HashMap中,那么我们如果new HashMap(1024),但是1024*0.75=768<1000,就会发生扩容。所以我们应该new HashMap(2048),因为2048*0.75=1536>1000

    核心方法

    构造方法

    空参构造,默认负载因子是0.75,在new HashMap时,并不会创建数组,而是在第一次调用put方法的时候创建

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    

    指定容量大小和默认负载因子

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    

    指定容量大小和指定负载因子(不建议改变负载因子)

    public HashMap(int initialCapacity, float loadFactor) {
        // 判断初始化容量 initialCapacity 是否小于0
        if (initialCapacity < 0) {
            // 如果小于 0,抛出非法的参数异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        }
        // 判断初始化容量 initialCapacity 是否大于集合的最大容量 MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY) {
            // 如果超过最大容量,将最大容量赋值给 initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
        }
        // 判断加载因子 是否小于等于0,或者是否是一个非法数值(NAN not a number)
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            // 如果满足上面条件,抛出非法参数异常
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        }
        // 将指定的负载因子赋值给 loadFactor
        this.loadFactor = loadFactor;
        /*
            tableSizeFor 判断指定的初始化容量是否为 2 的n次幂,
            如果不是,那就变为比指定容量大的最小的2的n次幂。
            但是注意,这里计算出初始化容量之后,直接赋值给了threshold
            有人认为这是个bug(原因主要是赋给边界值,要乘一个0.75)
            事实上,在put方法中,会对threshold重新计算
         */
        this.threshold = tableSizeFor(initialCapacity);
    }
    

    参数是Map的构造方法

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
    	final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            // 获取map的元素个数
            int s = m.size();
            if (s > 0) {
                // 判断 table是否已经初始化
                if (table == null) {
                    float ft = ((float) s / loadFactor) + 1.0F;
                    int t = ((ft < (float) MAXIMUM_CAPACITY) ?
                            (int) ft : MAXIMUM_CAPACITY);
                    // 判断得到的值是否大于阈值,如果大于阈值,则初始化阈值
                    if (t > threshold) {
                        threshold = tableSizeFor(t);
                    }
                } else if (s > threshold) {
                    // 已初始化,并且元素个数大于阈值,进行扩容
                    resize();
                }
                // 将m中所有的元素添加到HashMap中
                for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                    K key = e.getKey();
                    V value = e.getValue();
                    putVal(hash(key), key, value, false, evict);
                }
            }
        }
    

    put方法

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

    hash方法

    注意:HashMap允许key为空,但是 Hashtable 不支持 key =null

    为什么要右移16位?

    举一个例子

    00001111 00001111 00001111 11111111 //h=keyCode()
    00000000 00000000 00001111 00001111 //h>>>16
    00001111 00001111 00000000 11110000 //^
    00000000 00000000 00000000 00001111 //table.length-1
    00000000 00000000 00000000 00000000 //下标
    

    假设length的长度很小,(是16),那么length-1->1111,如果直接和hasCode()进行&操作,实际上只使用了hasCode()的四位。特别是当hasCode()的高位变化很大,低位变化很小,就很容易造成hash冲突。即为了减少hash冲突

    static final int hash(Object key) {
        int h;
        /*
            如果key为null
                可以看到当key为null的时候也是有哈希值的,返回值是0
            如果key不为null
                首先计算出key的hashCode,然后赋值给h,接着,h进行无符号右移16位,再进行异或运算
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    putVal方法

    	/**
         * @param hash         key的hash值
         * @param key          原始key
         * @param value        key对应的value
         * @param onlyIfAbsent 如果为true代表不更改现有的值
         * @param evict        如果为false,表示table为创建状态
         * @return previous value, or null if none
         */	
    	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K, V>[] tab;
            Node<K, V> p;
            int n, i;// n存放数组长度。i存放下标
            if ((tab = table) == null || (n = tab.length) == 0) {
                // 如果为空就通过resize实例化一个数组(前面谈到的在put中新建tab)
                n = (tab = resize()).length;
            }
            //数组为空直接存放
            if ((p = tab[i = (n - 1) & hash]) == null) {
                tab[i] = newNode(hash, key, value, null);
            } else {
                Node<K, V> e;//最终插入的节点
                K k;
                /*
                    比较桶中第一个元素的hash值和key是否相等。
                    
                    1. p.hash == hash :判断第一个元素的hash与我们传进来的hash是否相等
                    2. ((k = p.key) == key || (key != null && key.equals(k)))
                        2.1 (k = p.key) == key ==是地址比较,如果==都相等equals肯定也相等
                        2.2 (key != null && key.equals(k))) 值比较
                     上面如果都满足的情况下,说明第一个元素的key和我们传进来的key值是相等的
                 */
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k)))) {
                    // 将该位置的节点赋值给e
                    e = p;
                } else if (p instanceof TreeNode) {
                    // 判断当前下标位置的数据类型是否为红黑树
                    e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                } else {
                    // 说明当前元素是个链表
                    // 遍历链表
                    for (int binCount = 0; ; ++binCount) {
                        // 进入,说明e是表尾
                        if ((e = p.next) == null) {
                            // 直接将数据写到下一个节点
                            p.next = newNode(hash, key, value, null);
                            /*
                                1. 节点添加完成之后判断此时节点个数是否大于临界值 8,如果大于则将链表转为红黑树。
                                2. int binCount = 0,表示for循环的初始化值,从0开始计算,记录遍历节点的个数
                                    |- 0表示第一个节点
                                    |- 1表示第二个节点
                                    |- 。。。。
                                    |- 7表示第八个节点
                                    因此这里TREEIFY_THRESHOLD需要-1
                             */
                            if (binCount >= TREEIFY_THRESHOLD - 1) {
                                // 将链表转为红黑树
                                treeifyBin(tab, hash);
                            }
                            break;
                        }
                        // 如果当前位置的key与要存放位置的key相同,直接跳出
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k)))) {
                            /*
                                要添加的元素和链表中存在的元素相等了,则跳出for循环,不需要再比较后面的元素了
                                直接进入下面的if语句去替换e的值
                             */
                            break;
                        }
                        // 说明新添加的元素和当前节点不相同,继续找下一个元素。
                        p = e;
                    }
                }
                // e不为空,说明上面找到了一个去存储Key-Value的Node
                if (e != null) {
                    // 拿到旧Value
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null) {
                        // 新的值赋值给节点
                        e.value = value;
                    }
                    afterNodeAccess(e);
                    // 返回旧value
                    return oldValue;
                }
            }
            // 统计数据改变次数
            ++modCount;
            // 当最后一次调整之后的Size大于临界值,就需要调整数组容量
            if (++size > threshold) {
                resize();
            }
            afterNodeInsertion(evict);
            return null;
        }
    

    resize方法

    什么时候需要扩容?

    • HashMap中元素超过临界值(数组长度*负载因子)就会进行扩容。

    比如原数组长度是16,16*0.75=12,当元素个数大于12,则会进行扩容,变成32(扩大2倍)。所以当我们已知size的时候,应该要指定数组大小,避免扩容,消耗新能。

    • 当HashMap其中一个链表对象个数达到8个,此时如果数组长度没有达到64,HashMap也会进行扩容。

    HashMap的扩容是什么?

    HashMap在进行扩容的时候,使用rehash非常的巧妙。因为,每次扩容都是翻倍,与原来的(n-1)&hash的结果相比,只是多了一个二进制位,所以节点要么在原来的位置,要么就被分配到 原位置+原容量 这个位置。

    正是因为这样巧妙地rehash方式,既省去了重新计算hash的时间,而且同时,因为新增的1bit是0还是1可以认为是随机的,在resize的过程中保证了rehash之后每一个桶上的节点数一定小于等于原来桶上的节点数,保证了rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中。

    /**
         * 数组扩容
         */
        final Node<K, V>[] resize() {
            // 先拿到旧的hash桶
            Node<K, V>[] oldTab = table;
            // 获取未扩容前的数组容量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            // 旧的临界值
            int oldThr = threshold;
            // 定义新的容量和临界值
            int newCap, newThr = 0;
            // 旧容量大于0
            if (oldCap > 0) {
                // 旧的容量如果超过了最大容量
                if (oldCap >= MAXIMUM_CAPACITY) {
                    // 临界值就等于Integer类型最大值
                    threshold = Integer.MAX_VALUE;
                    // 不扩容,直接返回就数组
                    return oldTab;
                }
                /*
                    没超过最大值,数组扩容为原来的2倍
                    1.(newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大到2倍之后赋值给newCap,判断newCap是否小于最大容量
                    2.oldCap >= DEFAULT_INITIAL_CAPACITY 原数组长度大于等于数组初始化长度
                 */
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY) {
                    // 当前容量在默认值和最大值的一半之间
                    // 新的临界值为当前临界值的2倍
                    newThr = oldThr << 1; // double threshold
                }
            } else if (oldThr > 0) // initial capacity was placed in threshold
            {
                // 旧容量为0,当前临界值不为0,让新的临界值等于当前临界值
                newCap = oldThr;
            } else {
                // 当前容量和临界值都为0,让新的容量等于默认值,临界值=初始容量*加载因子
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            // 经过上面对新临界值的计算后如果还是0
            if (newThr == 0) {
                // 计算临界值为新容量 * 加载因子
                float ft = (float) newCap * loadFactor;
                // 判断新容量小于最大值,并且计算出的临界值也小于最大值
                // 那么就把计算出的临界值赋值给新临界值。否则新临界值默认为Integer最大值
                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];
            // 赋值给hash桶
            table = newTab;
            // 下面一堆是复制值
            // 如果旧的桶不为空
            if (oldTab != null) {
                // 遍历旧桶,把旧桶中的元素重新计算下标位置,赋值给新桶
                // j 表示数组下标位置
                for (int j = 0; j < oldCap; ++j) {
                    Node<K, V> e;
                    /*
                       (e = oldTab[j]) != null 将旧桶的当前下标位置元素赋值给e,并且e不为null
                     */
                    if ((e = oldTab[j]) != null) {
                        // 置空,置空之后原本的这个数据就可以被gc回收(*)
                        oldTab[j] = 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);
                        } else { // preserve order
                            // 到这里说明该位置的元素是链表
                            /*
                            loHead:链表头结点
                            loTail:数据链表
                            hiHead:新位置链表头结点
                            hiTail:新位置数据链表
                             */
                            Node<K, V> loHead = null, loTail = null;
                            Node<K, V> hiHead = null, hiTail = null;
                            Node<K, V> next;
                            // 循环链表,直到链表末再无节点
                            do {
                                // 获取下一个节点
                                next = e.next;
                                // 如果这里为true,说明e这个节点在resize之后不需要移动位置
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail != null) {
                                        loTail.next = e;
                                    } else {
                                        loHead = 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;
        }
    

    remove 方法

    /**
     * 根据key删除元素
     * 删除是有返回值的
     * 并且返回值是被删除key所对应的value
     */
    @Override
    public V remove(Object key) {
        Node<K, V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }
    

    方法中主要的方法是removeNode(hash(key), key, null, false, true),

    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;
        /*
            1. (tab = table) != null  把hash桶赋值给tab,并且判断tab是否为nul
            2. (n = tab.length) > 0 获取tab的长度,赋值给n,判断n是否大于0
            3. (p = tab[index = (n - 1) & hash]) != null 根据hash计算索引位置,赋值给index
                并从tab中取出该位置的元素,赋值给p,并判断,p不为null
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            // 进入这里面,说明hash桶不为空,并且当前key所在位置的元素不为空
            Node<K, V> node = null, e;
            K k;
            V v;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k)))) {
                // 当前第一个位置的元素就是我们要找的元素
                node = p;
            }
            // 取出p的下一个节点赋值给e,并且e不为空
            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);
                }
            }
            // 判断node不为空,
            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) {
                    // node==p,说明node是第一个节点,那么直接将下一个节点赋值给当前下标
                    tab[index] = node.next;
                } else {
                    p.next = node.next;
                }
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
    

    HashMap遍历方式

    1. 分别遍历key和value
    @Test
    public void testMap1() {
        HashMap<String, Integer> map = getMap();
        for (String key : map.keySet()) {
            System.out.println(key);
        }
        for (Integer value : map.values()) {
            System.out.println(value);
        }
    }
    
    1. 使用iterator迭代器迭代
    @Test
    public void testIterator() {
        HashMap<String, Integer> map = getMap();
        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }
    
    1. 通过get方式

    说明:根据阿里开发手册,不建议这种方式,因为要迭代多次。keySet一次,get一次

    @Test
    public void testGet() {
        HashMap<String, Integer> map = getMap();
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            System.out.println(key + ":" + map.get(key));
        }
    }
    
    1. Jdk8以后使用Map接口中的一个默认方法forEach
    @Test
    public void testForeach() {
        HashMap<String, Integer> map = getMap();
        map.forEach((key, value) -> {
            System.out.println(key + ":" + value);
        });
    }
    

    参考链接:

    https://blog.csdn.net/Elizabeth_ZSY/article/details/113434571

    https://blog.csdn.net/jdliyao/article/details/79826526

    https://ke.qq.com/course/1645879?taid=7384371633397047

    https://blog.csdn.net/chengqiuming/article/details/96692290

    https://www.iteye.com/blog/yananay-910460

    https://www.cnblogs.com/zhisuoyu/archive/2016/03/24/5314541.html

    https://blog.csdn.net/qq_25857759/article/details/88070241

  • 相关阅读:
    自己用的C++编码规范
    飘逸的python
    编译Sqoop2错误解决
    怎样设置linux中Tab键的宽度(可永久设置)
    系统分析师零散知识点
    Hadoop权威指南学习笔记一
    Spring获取request、session以及servletContext
    RequestContextHolder获取request和response
    Spring MVC 中RequestContextHolder获取request和response
    缓存清理
  • 原文地址:https://www.cnblogs.com/10134dz/p/14479118.html
Copyright © 2020-2023  润新知