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

posted @ 2021-03-04 11:11  DJ同学  阅读(66)  评论(0编辑  收藏  举报