写点什么

一篇文章深入理解 JDK7 HashMap

用户头像
itlemon
关注
发布于: 2020 年 07 月 09 日
一篇文章深入理解JDK7 HashMap

在日常开发中,集合作为存储数据的容器,被广泛使用在程序代码中,本文将从 JDK 集合类代表 HashMap 出发,着重理解 HashMap 底层实现。

一、Map 家族关系图

在正式讨论 HashMap 之前,我们有必要把 Map 家族的继承实现关系展示出来,方便理解后续的内容。


上图很详细地展示了 Map 家族中各个成员之间的继承或者实现关系。Map 接口是双列集合的顶层父接口,该集合中每个元素都是键值对 key-value,成对出现。双列集合中,一个键一定只找到对应的一个值,键不能重复,但是值可以重复。Map 集合常用的实现类有 HashMap、LinkedHashMap、HashTable,TreeMap。

  • HashMap

HashMap 的底层是哈希表+链表结构,与 HashSet 相似,只是数据的存储形式不同,HashMap 可以使用 null 作为 key 或 value,是线程不安全的,但是效率相对较高。当给 HashMap 中存放自定义对象时,如果自定义对象作为 key 存在,这时要保证对象唯一,必须重写对象的类的 hashCode 和 equals 方法。

  • HashTable

HashTable 与 HashMap 之间的关系完全类似于 Vector 和 Arraylist 的关系。HashTable 是线程安全的,但是效率相对较低,Hashtable 不允许使用 null 作为 key 和 value。

  • LinkedHashMap

LinkedHashMap 是 HashMap 的子类,其底层是哈希表+链表结构,其关系与 HashSet 和 LinkedHashSet 类似,使用链表来维护 key-value 的次序,可以记住键值对的插入顺序。

  • TreeMap

TreeMap 存储 key-value 键值对时,需要根据 key 对节点进行排序。TreeMap 可以保证所有的 key-value 对处于有序状态。也有两种排序方式:

自然排序:TreeMap 的所有 key 必须实现 Comparable 接口,而且所有的 key 应该是同一个类的对象,否则抛出 ClassCastException 异常。

定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。不需要 Map 的 key 实现 Comparable 接口。

本文主要介绍 Java7 中 HashMap 的底层实现原理,其他的 Map 集合,读者可以自行翻阅其他资料进行学习。

二、JDK7 中 HashMap 底层原理

2.1 HashMap 在 JDK7 中的结构

HashMap 在 JDK7 或者 JDK8 中采用的基本存储结构都是数组+链表形式。本节主要是研究 HashMap 在 JDK7 中的底层实现,其基本结构图如下所示:


上图中左边橙色区域是哈希表,右边蓝色区域为链表,链表中的元素类型为 Entry<K, V>,它包含四个属性分别是:


  • K key

  • V value

  • int hash

  • Entry<K, V> next


那么为什么会出现数组+链表形式的存储结构呢?这里简单地阐述一下,后续将以源码的形式详细介绍。

我们在使用 HashMap.put("Key", "Value")方法存储数据的时候,底层实际是将 key 和 value 以 Entry<Key, Value>的形式存储到哈希表中,哈希表是一个数组,那么它是如何将一个 Entry 对象存储到数组中呢?是如何确定当前 key 和 value 组成的 Entry 该存到数组的哪个位置上,换句话说是如何确定 Entry 对象在数组中的索引的呢?通常情况下,我们在确定数组的时候,都是在数组中挨个存储数据,直到数组全满,然后考虑数组的扩容,而 HashMap 并不是这么操作的。在 Java 及大多数面向对象的编程语言中,每个对象都有一个整型变量 hashcode,这个 hashcode 是一个很重要的标识,它标识着不同的对象,有了这个 hashcode,那么就很容易确定 Entry 对象的下标索引了,在 Java 语言中,可以理解 hashcode 转化为数组下标是按照数组长度取模运算的,基本公式如下所示:

int index = HashCode(key) % Array.length
复制代码

实际上,在 JDK 中哈希函数并没有直接采取取模运算,而是利用了位运算的方式来提高性能,在这里我们理解为简单的取模运算。

我们知道了对 Key 进行哈希运算然后对数组长度进行取模就可以得到当前 Entry 对象在数组中的下标,那么我们可以一直调用 HashMap 的 put 方法持续存储数据到数组中。但是存在一种现象,那就是根据不同的 Key 计算出来的结果有可能会完全相同,这种现象叫作“哈希冲突”。既然出现了哈希冲突,那么发生冲突的这个数据该如何存储呢?哈希冲突其实是无法避免的一个事实,既然无法避免,那么就应该想办法来解决这个问题,目前常用的方法主要是两种,一种是开放寻址法,另外一种是链表法。

开放寻址法是原理比较简单,就是在数组里面“另谋高就”,尝试寻找下一个空档位置,JDK 中 ThreadLocal 发生哈希冲突以后就是采用的开放寻址法。而链表法则不是寻找下一个空档位置,而是继续在当前冲突的地方存储,与现有的数据组成链表,以链表的形式进行存储。HashMap 的存储形式是数组+链表就是采用的链表法来解决哈希冲突问题的。具体的详细说明请继续往下看。

在日常开发中,开发者对于 HashMap 使用的最多的就是它的构造方法、put 方法以及 get 方法了,下面就开始详细地从这三个方法出发,深入理解 HashMap 的实现原理。

2.2 深入理解 HashMap 的构造方法

我们打开 IDE 查看 HashMap 的源码,首先得了解一下 HashMap 的一些成员属性,它的主要属性如下图所示:


这里对上面的属性进行简单地介绍:


1. 第一个属性是默认的初始化容量,表示哈希表的默认长度,当我们在创建 HashMap 对象的时候未指定容量时,默认的容量就是 16。这里多说一句,当我们知道我们的 Key-Value 对的个数的时候(一般是个数大于 16),我们在创建 HashMap 对象的时候最好就指定容量大小,很多人都认为,我们要存入多少 K-V 对就指定多少初始容量,其实这样是不对的,你指定的初始化容量不一定是最后真正的初始化容量,因为你设置的初始化容量是需要经过转换的,它会被转换成大于它本身且接近它的 2 的次幂,比如,我们需要在 HashMap 中存储 27 个 K-V 对,那么大于 27 且最靠近 27 的 2 的次幂就是 32,那么如果你在创建 HashMap 对象的时候指定的容量为 27,那么其实创建出来的 HashMap 容量为是 32,且当你连续存储到满了 24 个以后,当存入第 25 个 K-V 对的时候,有可能就会触发扩容,这样不仅没有减少扩容次数,反而有大概率地发生扩容。那么我们该如何确定传入的值呢?这里提供一个公式:(int)((float) expectSize / 0.75f + 1.0f),这个公式来自于 HashMap 的一个构造方法,比如你的expectSize = 27,那么计算的结果就是 37,那么就可以传入 37 作为初始化容量,经过内部计算以后,大于 37 且最接近 37 的 2 的次幂是 64,最终初始化出来的结果就是数组长度为 64,反向计算扩容阈值就是 48,那么存入 27 个 K-V 对是不会发生扩容的,如果仅仅是传入的 27,那么有很大可能会发生扩容,影响性能。

2. 第二个属性是哈希表的最大容量。

3. 第三个属性默认的负载因子,也就是我们在创建 HashMap 对象的时候没有指定负载因子,那么就使用这个负载因子,它是哈希表扩容的一个重要参数,当 HashMap 中存储的 K-V 对的个数大于等于哈希表容量乘以这个负载因子(size >= capacity * DEFAULT_LOAD_FACTOR)的时候,将触发扩容,且扩容为原容量的两倍。举个例子,当我们创建 HashMap 对象未指定哈希表容量的时候,也就是使用默认的 16,当调用 HashMap 对象的 put 方法存储数据的时候,当存储的数量为 16*0.75f=12 的时候,将触发扩容机制,此时哈希表容量将扩充为 32,这也就是在上面第一个属性描述的时候为什么多说一句的原因了。

4. 第四个属性是空的哈希表。

5. 第五个属性是后续操作的哈希表。

6. 第六个属性是 HashMap 中存储的 K-V 组合的个数。

7. 第七个属性是扩容阈值,当size >= threshold的时候(实际还需满足有链表的条件),将触发哈希表的扩容机制,threshold = capacity * loadFactor

8. 第八个属性是是自定义负载因子,当没有指定负载因子的时候,loadFactor = 0.75f

9. 第九个属性是记录了 map 新增/删除 K-V 对,或者内部结构做了调整的次数。其主要作用,是对 Map 的遍历操作做一致性校验,如果在 iterator 操作的过程中,map 的数值有修改,直接抛出 ConcurrentModificationException 异常。

10. 第十个属性表示对字符串键的 HashMap 应用代替哈希函数时,Map 内元素个数的默认阈值,目的是为了减少哈希碰撞的概率。


在 JDK7 中,HashMap 有四个构造方法,分别是:

// 该构造方法是第二第三构造方法的底层实现,该构造方法对初始化容量和负载因子进行了一个校验// 然后将传入的负载因子复制给了loadFactor成员变量// 将初始化容量赋值给了扩容阈值(扩容临界数值)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; threshold = initialCapacity; init();}
// 指定容量来创建HashMap对象,该变量initialCapacity不一定是初始化哈希表的长度public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}
// 使用默认的容量16和默认的负载因子0.75f来创建HashMap对象public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}
// 传入一个Map将其转化为HashMappublic HashMap(Map<? extends K, ? extends V> m) { // 创建一个HashMap this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); // 初始化HashMap,重点方法,后面详细介绍 inflateTable(threshold); // 将Map转化为HashMap的具体实现 putAllForCreate(m);}
复制代码

我们一起看看前三个构造方法,第二个第三个构造方法都是调用的第一个构造方法,这三个构造方法很简单,但是第一个构造方法有一点需要注意,那就是构造方法将初始化容量 initialCapacity 的值赋值给了扩容阈值 threshold,这就给我们带来一个疑问,一开始我们都知道一个等式关系:threshold=capacity*loadFactor,难道默认情况下,我们使用 HashMap 的无参构造方法,扩容阈值和默认容量一样,都是 16?难道第一次扩容是 size=16 才开始吗?这个疑问我们先放一放,稍后在 put 方法中你就知道到底是怎么回事了。我们先研究一下第四个构造方法,第四个构造方法是将一个 Map 转换成 HashMap,一共三行代码:

第一行代码创建了一个 HashMap 对象,设置了 threshold 值和 loadFactor 值。

第二行代码是初始化 HashMap,第三行是将 Map 转换成 HashMap 的具体实现。

一起来看看第二行代码内部实现:

private void inflateTable(int toSize) {    // 计算大于toSize的且最接近toSize的2的N次幂    // 比如toSize=27,那么大于27且最接近27的2的N次幂是32,N=5    int capacity = roundUpToPowerOf2(toSize);
// 这里重新计算了threshold,由代码可知,threshold = capacity * loadFactor threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 初始化了哈希表(桶数组) table = new Entry[capacity]; initHashSeedAsNeeded(capacity);}
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;}
复制代码

从第四个构造方法可知,转化过程中,暂时将确定的一个数组容量值赋值给 threshold 暂存,在初始化数组之前,计算出最合适的 capacity(大于 threshold 且最接近 threshold 的 2 的 N 次幂),然后再计算出真正的扩容阈值 threshold(threshold = capacity * loadFactor),然后在创建 Entry 数组(桶数组)。这里其实也对上面疑问进行了一个解答,其实我们在使用 HashMap 的无参构造创建 HashMap 对象的时候,并没有初始化 Entry 数组,那么是何时初始化 Entry 数组的呢?那是在第一次调用 put 方法的时候,后续,我们会一起深入理解 HashMap 的 put 方法。接下来,我们继续看 putAllForCreate 方法。

// 这就是将指定Map转换为HashMap的方法,循环遍历,主要看下面的putForCreate方法private void putAllForCreate(Map<? extends K, ? extends V> m) {    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())        putForCreate(e.getKey(), e.getValue());}

private void putForCreate(K key, V value) { // 计算hash值,如果key==null,那么hash值为0 int hash = null == key ? 0 : hash(key); // 利用hash值和哈希表长度计算当前k-v应在数组的索引下标 int i = indexFor(hash, table.length);
// 由于table[i]处可能不止有一个元素(多个会形成一个链表),因此,此处写这样一个循环遍历链表 // 当key存在的时候,直接将key的值设置为新值 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } }
// 当key在当前数组和所有链表中不存在的时候,就在table的指定位置创建一个Entry createEntry(hash, key, value, i);}
void createEntry(int hash, K key, V value, int bucketIndex) { // 获取当前数组的Entry链表头节点,并赋值给Entry<K,V> e Entry<K,V> e = table[bucketIndex]; // 创建一个新的Entry节点对象,并作为新的头节点,将老的头节点赋值给新头节点的next属性 // 这一点很重要,可以看Entry的构造方法,这表明了JDK7是头节点插入链表 table[bucketIndex] = new Entry<>(hash, key, value, e); // Entry对象数量+1 size++;}
复制代码

上面的代码分析很重要,对后续的 put 方法研究很有帮助,从上面的代码分析可知,我们从源码级别了解到 JDK7 中的链表新增的新成员是插入头节点的。

2.3 深入理解 HashMap 的 put 方法

对于 HashMap,我们平常使用最多的就是 put 方法了,从上面的源码分析可知,在创建 HashMap 对象后,并没有初始化哈希表,也就是 HashMap 的成员属性Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE = {},那这个哈希表是何时开辟空间的?答案很明显,那就是第一次使用 put 方法的时候。

public V put(K key, V value) {	// 首先判断哈希表table是否是空表,第一次put的时候,必然是空表,那么就进行初始化	// 如果我们使用的是HashMap的无参构造方法创建的对象,那么threshold就是16,	// 上面已经分析过inflateTable方法了,它主要是计算了最合适的capacity=16,threshold=12	// 并创建了一个新的Entry<K, V>[16]赋值给了table,也就是开辟了内存空间	// 从第二次put开始,table就不再是空数组了    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    // 判断key是否为null,如果为null,那么就执行存储key为null的逻辑,后续分析putForNullKey方法    if (key == null)        return putForNullKey(value);    // 计算key的hash值    int hash = hash(key);    // 根据hash值和哈希表长度来计算当前K-V在哈希表中的索引下标    int i = indexFor(hash, table.length);    // 找到下标之后,获取当前下标对应的是否有Entry对象,如果有,那么就遍历这个链表    // 判断链表中是否已经包含当前key了,如果包含,那么就将key对应的值替换成新值,并返回旧值    for (Entry<K,V> e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }	// 能运行到这一步,说明上述链表不存在或者链表中没有对应的key	// 那么就需要添加新的Entry对象到指定的索引位置i上	// 记住这里也是头节点插入    modCount++;    // 新增Entry,不仅新增,而且还担任了扩容的担子,后续详细分析    addEntry(hash, key, value, i);    // 新增Entry对象,返回null    return null;}
复制代码

上面对 put 方法进行了解析,接下来我们深入理解一下 putForNullKey 方法和 addEntry 方法,首先我们一起理解一下 putForNullKey 方法,代码如下:

private V putForNullKey(V value) {    for (Entry<K,V> e = table[0]; e != null; e = e.next) {        if (e.key == null) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    addEntry(0, null, value, 0);    return null;}
复制代码

该方法是对 key 为 null 的特殊处理的方法,在 HashMap 设计的时候,就规定了哈希表第一个位置,也就是下标为 0 的位置,只存放 key=null 的 Entry 对象,这也就合理解释了 HashMap 可以存储 key 为 null 的 K-V 对,且最多只存储一个 key 为 null 的 Entry 对象。

接下来一起看看 addEntry 方法,该方法不仅担任了添加 Entry 对象任务,还担任了扩容的任务。

void addEntry(int hash, K key, V value, int bucketIndex) {	// 扩容的条件:1. size >= threshold	// 2. null != table[bucketIndex]	// 两个条件同时满足才会进行扩容    if ((size >= threshold) && (null != table[bucketIndex])) {    	// 扩容为原来容量的两倍,具体代码后续分析        resize(2 * table.length);        // 重新计算当前要新增的K-V对在新的哈希表中的索引位置        hash = (null != key) ? hash(key) : 0;        bucketIndex = indexFor(hash, table.length);    }
// 创建Entry对象,并插入到指定位置的头节点 createEntry(hash, key, value, bucketIndex);}
复制代码

上面的两处代码分析可以得出一个结论:

扩容的条件必须满足size >= thresholdnull != table[bucketIndex],看到很多资料说只要size >= threshold就会触发扩容机制,这是不对的。看条件就知道,size >= threshold中的等于号一定会满足,只要一直执行 put 方法,那么一定就会有等于的时候,那么为啥还需要大于号呢?按照错误的想法,岂不是大于号是多此一举?其实不然,扩容要满足两个条件,如果 put 方法在 put 第 threshold 个 K-V 对的时候,但是存放 Entry 对象的数组 bucketIndex 处并没有链表,那么也不会扩容,也就是说,put 第 threshold 个 K-V 对且发生哈希冲突才会扩容。引申一点,当 put 键为 null 的 K-V 对的时候,永远不会发生扩容,因为它要么发生 Value 的替换,那么是一个 Entry 对象的插入,不会涉及到哈希冲突。

进一步总结如下:


  • 扩容条件必须同时满足size >= thresholdnull != table[bucketIndex]

  • put 键为 null 的 K-V 对的时候永远不会发生扩容

2.4 深入理解 HashMap 的扩容机制

从上面小节的分析可知,扩容的条件为:


  • 扩容条件必须同时满足size >= thresholdnull != table[bucketIndex]

  • put 键为 null 的 K-V 对的时候永远不会发生扩容


我们查看 resize 方法的具体实现来理解 HashMap 的扩容机制,代码如下:

void resize(int newCapacity) {    Entry[] oldTable = table;    int oldCapacity = oldTable.length;    // 如果老数组的容量达到了最大,那么就将threshold设置为最大值,且不会再发生扩容    if (oldCapacity == MAXIMUM_CAPACITY) {        threshold = Integer.MAX_VALUE;        return;    }
// 创建一个新的哈希表,容量是原来哈希表的两倍,也就是newCapacity = 2 * oldCapacity Entry[] newTable = new Entry[newCapacity]; // 将重新计算所有Entry对象的下标,并重新排列各个Entry对象的位置,稍后分析transfer方法实现 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; // 重新计算新的扩容阈值threshold threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}
复制代码

扩容很简单,就是将哈希表的长度变成原来的两倍,但是这仅仅是第一步,后面还需要对原有的数据进行迁移,使原有数据更加均匀地散列在哈希表中。数据迁移的具体操作由方法 transfer 来提供。

void transfer(Entry[] newTable, boolean rehash) {    int newCapacity = newTable.length;    // 遍历老的table,遍历到每一个bucket的时候,都会将bucket上链表的节点都遍历一遍    for (Entry<K,V> e : table) {        while(null != e) {            Entry<K,V> next = e.next;            // 重新计算hash            if (rehash) {                e.hash = null == e.key ? 0 : hash(e.key);            }            // 重新计算下标            int i = indexFor(e.hash, newCapacity);            // 头节点插入链表            e.next = newTable[i];            newTable[i] = e;            // 继续原链表的下一个节点            e = next;        }    }}
复制代码

这就基本完成了对 HashMap 的 put 方法的研究,其实研究起来并不是很难理解,耐下心来阅读一下源码,肯定会有很大收获。

2.5 深入理解 HashMap 的 get 方法

接下来我们继续来阅读 get 方法,相对于 put 方法,get 方法实现起来就简单很多,理解起来也不是很困难,基本原理就是计算 key 的 hash 值,然后计算当前 key 在哈希表中的索引位置,然后在遍历链表,逐个比对,最后返回结果。

public V get(Object key) {	// 首先判断key是否为null,如果为null,那么直接获取哈希表的下标为0的位置的Entry对象    if (key == null)        return getForNullKey();        // 当key != null,命中到哈希表中的某个下标位置后,遍历链表来获取key相同的entry对象    Entry<K,V> entry = getEntry(key);
// 处理结果并返回,如果找到就返回对应的值,如果没有匹配到直接返回null return null == entry ? null : entry.getValue();}
// 处理key = null的值问题private V getForNullKey() { if (size == 0) { return null; } // 返回哈希表中下标为0的Entry对象并处理返回结果 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null;}
// 遍历链表匹配到相同key的Entry对象并返回该Entry对象的value值final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; }
int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null;}
复制代码

getEntry 方法是遍历链表来获取与当前 key 相同的 Entry 对象,判断 key 是否存在的标准是:

e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))

节点的 key 和目标 key 的 hash 值肯定是相等的,&&右边的条件,即节点的 key 与目标 key 的相等,要么内存地址相等,要么逻辑上相等,两者有一个满足即可。

假设 get 方法传入的 key 值计算的下标为 1,HashMap 的 get 方法的基本示意图如下所示:


2.6 HashMap put、get 方法流程图

这里提供一个 HashMap 的 put 方法存储数据的流程图供读者参考:



这里提供一个 HashMap 的 get 方法获取数据的流程图供读者参考:



上面中 get 流程图画得稍微比正常的要复杂一些,只是为了描述流程更加清晰。

2.7 常见的 HashMap 的迭代方式

在实际开发过程中,我们对于 HashMap 的迭代遍历也是常见的操作,HashMap 的迭代遍历常用方式有如下几种:


  • 方式一:迭代器模式

Map<String, String> map = new HashMap<>(16);Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()) {    Map.Entry<String, String> next = iterator.next();    System.out.println(next.getKey() + ":" + next.getValue());}
复制代码
  • 方式二:遍历 Set<Map.Entry<K, V>>方式

Map<String, String> map = new HashMap<>(16);for (Map.Entry<String, String> entry : map.entrySet()) {    System.out.println(entry.getKey() + ":" + entry.getValue());}
复制代码
  • 方式三:forEach 方式(JDK8 特性,lambda)

Map<String, String> map = new HashMap<>(16);map.forEach((key, value) -> System.out.println(key + ":" + value));
复制代码
  • 方式四:keySet 方式

Map<String, String> map = new HashMap<>(16);Iterator<String> keyIterator = map.keySet().iterator();while (keyIterator.hasNext()) {    String key = keyIterator.next();    System.out.println(key + ":" + map.get(key));}
复制代码

把这四种方式进行比较,前三种其实属于同一种,都是迭代器遍历方式,如果要同时使用到 key 和 value,推荐使用前三种方式,如果仅仅使用到 key,那么推荐使用第四种。

三、总结

本文着重讲解了 JDK7 中 HashMap 的具体实现原理,包括 put、get、扩容等内部实现机制,相信读者仔细品读以后,对 JDK7 中的 HashMap 的实现会有一个清晰地认识,JDK7 中的 HashMap 的实现原理属于经典实现,不管 JDK7 是否已经再被使用,但是其基本原理还是值得学习!后续将继续讲解 JDK8 中的 HashMap 实现原理,届时将对比 JDK7,帮助读者掌握两者之间的共性和差异!


了解更多干货,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)


发布于: 2020 年 07 月 09 日阅读数: 76
用户头像

itlemon

关注

还未添加个人签名 2018.09.30 加入

Java码农一枚。

评论

发布
暂无评论
一篇文章深入理解JDK7 HashMap