HashMap jdk1.7源码阅读与解析

转载自  HashMap源码阅读与解析

一、导入语

HashMap是我们最常见也是最长使用的数据结构之一,它的功能强大、用处广泛。而且也是面试常见的考查知识点。常见问题可能有HashMap存储结构是什么样的?HashMap如何放入键值对、如何获取键值对应的值以及如何删除一个键值对。今天我们就来看看HashMap底层的实现原理。下面我们就开始进入正题,分析一下hashmap源码的实现原理。

 

二、HashMap构造方法以及存储结构


public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init();
}

HashMap的构造方法有好几个,在这里我们就不一一介绍,只说一下我们最常见的HashMap无参构造方法。上面的构造方法中,有几个变量需要我们这里说明一下:

  1. loadFactor:加载因子,默认值为0.75;
  2. threshold:threshold是一个阈值,初始值为默认为16*0.75。当hashmap中存放键值对数量大于该值时,表示hashmap容量大小需要扩充,一般容量会翻倍。
  3. table:table其实是一个Entry类型的数组,在hashmap中我们利用数组和链表来解决hash冲突,这里的table数组用于存放冲突链表的头结点。

另外在HahsMap中,我们通过数组加链表的方式来存储Entry节点(Entry数据结构用于存储键值对)。这里所谓的数组即是上面提到的table,它是一个Entry数组,table对象中节点初始化值均为null,当我们新插入的节点第一次散列到该位置时,会将节点插入到table中对应位置。如果后续存在散列位置相同的节点,会以链表的方式解决hash冲突。示意图如下:

 

三、put()方法解析

put方法是我们最常用方法,我们利用该方法将键值对放入HashMap集合中,那么HashMap到底是什么样的结构,put()方法又做了什么呢?我们下面就来看看put()方法的具体实现。


public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);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;}}modCount++;addEntry(hash, key, value, i);return null;
}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;
}

if (key == null)return putForNullKey(value);

如果当前传入的key值为null,执行putForNullKey()方法;当key值为null时,hash值为0,将其保存到以table[0]为开头的链表中去。遍历链表,如果存在某节点的key值为null,则用新value直接将其替换。如果未找到key值为null的节点,调用addEntry()方法插入一个key为null的新节点。addEntry方法我们会在后文中介绍。


int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);

为什么这里还要对key的hashCode值再调用一次哈希算法呢?简单来说就是为了让传递进来的key散落位置可以更加均匀,具体原因就不在本文中介绍了,网上有很多资料可供借鉴。
接着调用indexFor方法计算当前key值散落在table中的位置,其实就是key%table.length


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

遍历以table[i]为头结点的链表,查找是否已经有相同的key值的节点存在于链表中。判断条件为if (e.hash == hash && ((k = e.key) == key || key.equals(k)))。这个判断条件十分重要,我们来仔细分析下。首先是e.hash == hash:之前我们已经计算出了当前待处理节点的hash值,并保存在变量hash中,在此我们需要比较当前链表遍历节点key的hash值(e.hash)和hash是否相等。如果我们去看一下addEntry()方法我们会发现,Entry节点的存储位置实际上是由key的hash值来决定的。如果key的hash相同,那么他们的存储位置也相同。(k = e.key) == key || key.equals(k))。先简单的说一下”==”和”equals”的意义,”==”是引用一致性判断,而equals是内容一致性判断。这里的意思也就是说如果两个key对象指向的是同一个对象,或者他们就是同一个对象,则返回true。总结一下,如果hash值相同,则key值相同或是同一个对象的引用,则表示hashmap中存在以key为键值的Entry节点。
如果判断if (e.hash == hash && ((k = e.key) == key || key.equals(k)))判断条件返回为true,则用新值替换老值。

如果没有找到相同的key值,则调用addEntry()方法新增一个指定key和value的Entry节点。

 

四、addEntry()方法解析


void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<K,V>(hash, key, value, e);if (size++ >= threshold)resize(2 * table.length);
}

接下来继续看addEntry()方法,假设当前节点为插入到table[bucketIndex]位置的第一个节点


Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

在Entry类的构造方法中有这样一句代码:


next = e;

即当前新建的entry节点将指向Entry构造方法传递过来的Entry节点e,此时e保存的值为头结点的值,也就是null。该节点创建完之后,又被赋值给table[bucketIndex],相当于链表的头结点了保存了最新插入的节点。如下图所示我们在table[i]位置插入了Entry节点。

如果此时新来一个key2节点,经过散列之后其散落的位置和key1相同。此时key1和key2的散落位置发生了冲突,我们将采用链表来解决该冲突。
还是看那两句代码:


Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  1. 此时table[buckertIndex]中存放的节点为,将其赋值给e
  2. 新建一个Entry节点,key=”key2”,value=”value2”,同时该entry节点next值指向,同时将table[bucketIndex]的值也被赋为新节点。
    示例图如下图所示。

    我们从上面往hashmap中放键值对的过程中可以发现,所有的键值对信息其实都是通过Entry节点来保存的,发生冲突的节点会通过一个链式结构进行保存。同时table[bucketIndex](相当于头结点)总是保存最后被放入该位置的键值对信息。

另外在addEntry方法中有如下两句代码


if (size++ >= threshold)resize(2 * table.length);

size的值为当前hashMap中存储的节点个数,threshold是一个阈值。如果hashMap中存储的节点个数大于等于threshold,表示我们需要对当前hashMap进行扩容了。每一次扩充容量为之前容量的2倍。我们来看一下resize()方法。

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}Entry[] newTable = new Entry[newCapacity];transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);}}
}

关键代码是这一段


Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;

如果resize()之前Entry数组的大小为A,那么newTable数组的大小为2A
transfer(newTable)方法用于将原先entry[]数组中的节点转移到newTable数组中,下面我们来看下transfer()方法具体干了什么。

  1. 将原来的table数组赋值给src数组
  2. 获取newTable数组的长度,这里为table数组长度的2倍
  3. 循环遍历src数组,执行下面的操作

a. 取src[j]节点的值赋值给e

b. 如果e节点不为null,将src[j]的值置为null

我们来举两个简单的例子说明一下tranfer到底干了什么:
当src[j]不为空时,比方说src[j]中保存的Entry节点key=”key2”,value=”value2”,src[j]指向的下一个节点key=”key1”,value=”value1”,如下图所示:

  1. 最开始的时候newTable[]中并没有存放任何Entry节点,只是单纯的进行了初始化。结合上面代码,我们可以看到此时e = entry2节点,next节点值为entry1
  2. 利用indexFor重新计算出e节点的散列位置。e节点的next指向被初始化后的newTable[i]节点,同时newTabel[i]的值也被赋值为e节点
  3. 最后执行e = next;此时e等于entry1
    形成节点的示意图如下:

    接着执行
  4. next = e.next,此时e的next节点为null,next =null;
  5. 利用indexFor计算出新的散列位置,比如说新的散列位置为j,此时以newTable[j]为头节点的链表中已经存在了两个节点。如下图所示:

    我们将待处理的节点entry节点插入后会变成什么样呢?

    简单的来说resize方法就是去逐个遍历table[i]后面的Entry节点链表,利用indexFor方法重新结算节点的散落位置,并将其插入到以newTable[]为头结点的链表中去。

 

五、get()方法解析

说完了put我们再来看一下get方法


public V get(Object key) {if (key == null)return getForNullKey();int hash = hash(key.hashCode());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.equals(k)))return e.value;}return null;
}private V getForNullKey() {for (Entry<K,V> e = table[0]; e != null; e = e.next) {if (e.key == null)return e.value;}return null;
}

理解了put方法时如何往hashmap中放入键值对的,那么get()方法也就很好理解了。我们来具体看看get()方法的实现。

  1. 如果key值为null,执行getForNullKey()方法。当key值为null时,新的键值对会放到table[0]处,所以我们先去遍历table[0]位置的节点链表,查看是否有key值为null的节点。如果有的话,直接返回value。如果找不到key为null的节点,返回null。
  2. 如果key值不为null,利用indexFor方法找到当前key所处的table[i]位置,遍历table[i]位置的节点链表。根据e.hash == hash && ((k = e.key) == key || key.equals(k))来判断是否有相同key值的节点。如果当前位置链表中存在key值相同的Entry节点,返回Entry节点保存的value。如果找不到key值匹配的Entry节点,返回null。

 

六、remove()方法解析


public V remove(Object key) {Entry<K,V> e = removeEntryForKey(key);return (e == null ? null : e.value);
}final Entry<K,V> removeEntryForKey(Object key) {int hash = (key == null) ? 0 : hash(key.hashCode());int i = indexFor(hash, table.length);Entry<K,V> prev = table[i];Entry<K,V> e = prev;while (e != null) {Entry<K,V> next = e.next;Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {modCount++;size--;if (prev == e)table[i] = next;elseprev.next = next;e.recordRemoval(this);return e;}prev = e;e = next;}return e;
}

别看remove方法这么长,其实它的逻辑很简单

  1. 通过hash()和IndexFor()方法找到当前Entry节点的散列位置i,prev节点为当前节点的上一个节点(初始值为table[i]节点),e节点表示当前节点。
  2. 比较待删除节点的key值和当前节点的key值是否相符。如果找不到相符的节点,返回null;
    如果有相符的节点,且为头结点,e节点的下一个节点将被赋值给table[i];
    如果有相匹配的节点,并且不为头结点,则prev节点不再指向e,而是指向e.next,也即是prev.next = e.next;相当于一个断链操作;

 

七、HashMap遍历

如果让你写一个hashmap的遍历代码,估计大部分人写出下面这段代码。可是HashMap的遍历过程到底是怎么样的,为什么我们每次取值的时候都使用iter.next()来取值的呢?下面我们就来看看HashMap的遍历实现。


    Itreator iter = map.entrySet().itreator();while(iter.hashNext()){Map.entry<k,v> entry = (Map.entry<k,v>) iter.next();
}

HashMap类中有一个私有类EntrySet,它继承自AbstractSet类。EntrySet类中有一个iterator()方法,也就是我们上面在遍历hashMap所调用的iterator()方法,它会返回一个Iterator对象。
我们来看看iterator方法:


public Iterator<Map.Entry<K,V>> iterator() {return newEntryIterator();
}

iterator()方法中调用了newEntryIterator()方法,接着进入newEntryIterator()方法看看。


    Iterator<Map.Entry<K,V>> newEntryIterator()   {return new EntryIterator();
}

newEntryIterator方法又创建了一个EntryIterator对象并返回。这个EntryIterator很关键,我们来具体看看这个类。


private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {public Map.Entry<K,V> next() {return nextEntry();}
}

EntryIterator类继承自HashItertor类,而且HashIterator类只有一个方法next()。既然EntryIterator继承自HashIterator类,那么EntryIterator到底继承了父类的哪些对象,默认实现了父类的哪些方法呢?我们再看看HashIterator类。


private abstract class HashIterator<E> implements Iterator<E> {Entry<K,V> next;    // next entry to returnint expectedModCount;   // For fast-failint index;      // current slotEntry<K,V> current; // current entryHashIterator() {expectedModCount = modCount;if (size > 0) { // advance to first entryEntry[] t = table;while (index < t.length && (next = t[index++]) == null);}}
}

HashIterator类中有四个属性,它们的用处代码注释已经简单明了的介绍了。值得注意的是HashIterator()提供了一个无参的构造方法,然而他并没有对所有的属性进行初始化,在这里我们需要明确的是index的值将会被赋为0。同时后面还有一大段,它干了什么呢?

  1. 首先是Entry[] t = table;将当前存储头结点的Entry[]数组table赋值给t;
  2. 接着执行一个while循环

    
       while (index < t.length && (next = t[index++]) == null)

    当index大于table的长度,或者当前t[index]位置保存的节点不为空时,将会结束while循环。也就是说该循环目的是为了找出table[]数组中第一个存储了Entry对象的位置,并用index变量记录该位置。
    我们再总结一下!当Itreator iter = map.entrySet().itreator();这句代码结束之后,我们获得了一个Iterator对象,这个对象保存了当前hashMap的modCount值,index用于标识table[]数组中第一个不为null的位置,同时next的初始值也等同于table[index]的值。


while(iter.hashNext())

当前对象实际上为HashIterator对象,HashIterator对象的hasNext()方法十分的简单


public final boolean hasNext() {return next != null;
}

Map.entry<k,v> entry = (Map.entry<k,v>) iter.next();

再梳理一下逻辑,EntryIterator 有一个方法next


public Map.Entry<K,V> next() {return nextEntry();
}final Entry<K,V> nextEntry() {if (modCount != expectedModCount)throw new ConcurrentModificationException();Entry<K,V> e = next;if (e == null)throw new NoSuchElementException();if ((next = e.next) == null) {Entry[] t = table;while (index < t.length && (next = t[index++]) == null);}current = e;return e;
}

如果modCount值不等于expectedModCount,表示在当前遍历过程中,HashMap可能被其他线程修改过,我们需要抛出ConcurrentModificationException异常,这也就是我们常说fast-fail。同时新建一个Entry节点e,赋值为next(第一次进来是next指向的就是table[]数组中第一个不为null的头结点)。
如果说当前节点的下一个节点为null,相当于遍历到了当前table[i]所指向链表的最后一个节点。此时我们应当去寻找table数组中下一个头结点不为null的位置。
执行while (index < t.length && (next = t[index++]) == null) 找到下一个不为null的头结点,并保存到next节点中。
返回当前节点e

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327483.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java实现加密电话号码,有具体的加密流程注释

闲着没事做&#xff0c;正好有一位哥们让帮他看个写个逻辑题&#xff0c;我就顺便写了下&#xff01; 此题主要是加密一个数字类型的电话号码&#xff0c;具体加密流程如下&#xff1a; * 将一串数字进行加密 * 加密规则&#xff1a;先把这串数字降序&#xff0c;然后将每个…

.NET项目版本号的小随笔

【题外话】 一直以来都对.NET项目中的几个版本号&#xff08;AssemblyVersion、AssemblyFileVersion、AssemblyInformationalVersion&#xff09;以及版本号中的Revision和Build有疑问&#xff0c;今儿抽了点时间看了几篇文章&#xff0c;整理一下与大家一起分享下。 【一、Ass…

Windows.etc\hosts文件

Windows.etc\hosts文件 ZC&#xff1a;就是将 后面的项 重定位到 前面的项 1、目录&#xff1a;"C:\Windows\System32\drivers\etc" 文件&#xff1a;"C:\Windows\System32\drivers\etc\hosts" 2、c__Windows_System32_drivers_etc_hosts的作用 - Sharpe…

java实现邮件发送准备工作(前期配置)

本文主要用的邮件客户端是&#xff1a;office 2007的outlook,服务器是apache-james-2.3.2&#xff0c;首先我们来配置一下这个james服务器: 1.将james服务器解压到硬盘目录下&#xff0c;注意目录不能有中文&#xff0c;如e:盘下 2.修改apps/james/sar-inf目录下的confi…

win10关闭“Windows安全中心”功能的两种方法

win10系统怎么将windows安全中心关闭&#xff1f; 听语音 原创|浏览&#xff1a;10407|更新&#xff1a;2020-03-24 10:541 2 3 4 5 6 7 分步阅读 一些软件需要将Windows安全中心关闭。 方法/步骤 1 首先打开开始菜单。 2 在开始菜单中点击设置按钮。 3 在设置界…

ASP.NET Core 中间件Diagnostics使用

ASP.NET Core 中间件(Middleware)Diagnostics使用。对于中间件的介绍可以查看之前的文章ASP.NET Core 开发-中间件(Middleware)。 Diagnostics中间件&#xff0c;主要功能是用于报告和处理ASP.NET Core中的异常和错误信息&#xff0c;以及诊断Entity Framework核心迁移错误。 其…

使用java底层实现邮件的发送(含测试,源码)

直接上代码&#xff1a;3个类&#xff0c;两个主要的类&#xff0c;一个测试类&#xff1a; 主类&#xff08;Mail&#xff09;&#xff1a; /** * Title: Mail.java * Package org.service.impl * Description: TODO该方法的主要作用&#xff1a; * author A18ccms A18ccms_…

Java多线程:线程状态

转载自 Java多线程:线程状态 一. 线程状态类型 1. 新建状态&#xff08;New&#xff09;&#xff1a;新创建了一个线程对象。 2. 就绪状态&#xff08;Runnable&#xff09;&#xff1a;线程对象创建后&#xff0c;其他线程调用了该对象的start()方法。该状态的线程位于可运行…

Win10怎么关闭开机启动项

Win10怎么关闭开机启动项 我们可以首先打开电脑的运行对话框&#xff0c;按下键盘的WINR组合键&#xff0c;打开运行。 然后这里我们在运行对话框中输入命令msconfig确定&#xff0c;打开系统配置程序。 系统配置窗口&#xff0c;启动里面点击这里的任务管理器打开。 这时…

TypeScript 2.1发布

TypeScript是微软开发的一个JavaScript的超集&#xff0c;提供了最新的JavaScript特性以及可选的静态类型。近日&#xff0c;TypeScript 2.1发布。该版本提供了功能更为强大的类型检查器&#xff0c;并且让开发人员可以编写出更简洁的代码。以下是该版本带来的主要新特性&#…

使用spring实现邮件的发送(含测试,源码,注释)

此篇主要讲的是使用spring配置实现邮件发送&#xff0c;与之前的底层实现简便了不少&#xff0c;只需要几个配置就可以了&#xff0c;那么请往下看&#xff1a; 先写个接口 /** * Title: IMailserdService.java * Package org.service * Description: TODO该方法的主要作用&a…

你当前无权访问该文件夹 解决你当前无权访问该文件夹拒绝你访问该文件夹

我 这样就完成了 http://www.xitonghe.com/jiaocheng/windows7-5642.html https://jingyan.baidu.com/article/4b52d702aa01b3fc5c774b1b.html Win10正式版提示你当前无权访问该文件夹怎么办 https://jingyan.baidu.com/article/4b52d702aa01b3fc5c774b1b.html 1407345人看了…

SpringBoot+Vue博客系统---后端接口开发

Java后端接口开发 从零开始搭建一个项目骨架&#xff0c;最好选择合适&#xff0c;熟悉的技术&#xff0c;并且在未来易拓展&#xff0c;适合微服务化体系等。所以一般以Springboot作为我们的框架基础&#xff0c;这是离不开的了。 然后数据层&#xff0c;我们常用的是Mybati…

图说世界编程语言排行

TIOBE编程语言社区排行榜是编程语言流行趋势的一个指标&#xff0c;每月更新&#xff0c;这份排行榜排名基于互联网上有经验的程序员、课程和第三方厂商的数量。排名使用著名的搜索引擎&#xff08;诸如Google、MSN、Yahoo!、Wikipedia、YouTube以及Baidu等&#xff09;进行计算…

中国有超级计算机的大学,计算机专业排名看超算实力,ASC竞赛五大高校排名,中山大学第一...

ASC竞赛五大高校计算机专业的实力主要体现在算法与编程的逻辑运算上&#xff0c;因此计算机专业必须掌握大量基础数学知识&#xff0c;甚至很多是离散数学、模糊数学等人工智能逻辑数学&#xff0c;简单的程序软件应用和O2O程序实现其实都不是计算机专业实力的体现&#xff0c;…

IntelliJ IDEA设置JDK版本

IntelliJ IDEA设置JDK版本 临渊行 2019-06-13 13:59:13 46888 收藏 30 分类专栏&#xff1a; 这里有个坑 版权 一、背景 即使我电脑安装的JDK版本是8&#xff0c;然而在idea运行中常常提示xxjdk1.5已过时之类的&#xff0c;why?明明是我装的JDK8啊 二、解决 鼠标点击f…

从抵触到力推,.Net Core的成功让微软正视开源

微软在两年前做了一件当时非常令人惊讶的事情&#xff1a;将 .Net Core 开源。在此之前&#xff0c;微软曾表示开源是对软件经济的威胁。或许是为了更好的销售工具和云服务&#xff0c;亦或许是为了吸引更多的开发者到其平台&#xff0c;微软开始开源。到目前为止&#xff0c;这…

线程池的原理及实现

转载自 线程池的原理及实现 1、线程池简介&#xff1a; 多线程技术主要解决处理器单元内多个线程执行的问题&#xff0c;它可以显著减少处理器单元的闲置时间&#xff0c;增加处理器单元的吞吐能力。 假设一个服务器完成一项任务所需时间为&#xff1a;T1 创建线…

计算机考试行高怎么设置,Excel隔行调整行高的四种有效方法

领导要求把一份Excel表格的偶数行行高调整一下。这份表格可是有上百行的&#xff0c;逐一调整行高显然是不科学的。几经周折&#xff0c;费了我N多的口舌四处讨教&#xff0c;这个任务还是顺利地完成了。一、直接定位法先在表格的最后增加一个辅助列。在该列的第一行的单元格中…

亲身体验Intellij Idea从卡顿到顺畅

亲身体验Intellij Idea从卡顿到顺畅 idea power save mode 指尖飘落的程序 2018-01-16 14:12:12 11991 收藏 1 分类专栏&#xff1a; 工具 版权 power save mode 开启后代码不提示&#xff0c; 省电模式&#xff08;经典模式&#xff1a;适用低配版电脑&#xff09; htt…