HashMap源码剖析

 无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在。平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的。

一、HashMap概述

  HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

1 Map map = Collections.synchronizedMap(new HashMap());

二、HashMap的数据结构

  HashMap的底层 主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置,能够很快的计算出对象所存储的位置。 HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多 了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很 多,HashMap底层是通过链表来解决hash冲突的。

从上图中可以看出,HashMap底层就是一个数组结构,数组中存放的是一个Entry对象,如果产生的hash冲突,也就是说要存储的那个位置上面已经存储了对象了,这时候该位置存储的就是一个链表了。我们看看HashMap中Entry类的代码:
 1 static class Entry implements Map.Entry {2         final K key;3         V value;4         Entry next;5         final int hash;67         /*8           Creates new entry.9          */
10         Entry(int h, K k, V v, Entry n) {
11             value = v;
12             next = n; //hash值冲突后存放在链表的下一个
13             key = k;
14             hash = h;
15         }
16
17         ………
18     }

  HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

 

三、HashMap源码分析

  先看看HashMap类中的一些关键属性:

1 transient Entry[] table;//存储元素的实体数组
2
3 transient int size;//存放元素的个数
4
5 int threshold; //临界值   当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
6
7 final float loadFactor; //加载因子
8
9 transient int modCount;//被修改的次数

  其中加载因子是表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,
好 处是:冲突的机会减小了,但:空间浪费多了.冲突的机会越大,则查找的成本越高.反之,查找的成本越小.因而,查找时间就越小.因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的”时-空”矛盾的平衡与折衷.

  如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

  下面看看HashMap的几个构造方法:

 1     public HashMap(int initialCapacity, float loadFactor) {2         //确保数字合法3         if (initialCapacity < 0)4             throw new IllegalArgumentException(“Illegal initial capacity: “ +5                                                initialCapacity);6         if (initialCapacity > MAXIMUM_CAPACITY)7             initialCapacity = MAXIMUM_CAPACITY;8         if (loadFactor <= 0 || Float.isNaN(loadFactor))9             throw new IllegalArgumentException(“Illegal load factor: “ +
10                                                loadFactor);
11
12         // Find a power of 2 >= initialCapacity
13         int capacity = 1;   //初始容量
14         while (capacity < initialCapacity)   //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
15             capacity <<= 1;
16
17         this.loadFactor = loadFactor;
18         threshold = (int)(capacity  loadFactor);
19         table = new Entry[capacity];
20         init();
21     }
22
23     public HashMap(int initialCapacity) {
24         this(initialCapacity, DEFAULT_LOAD_FACTOR);
25     }
26
27     public HashMap() {
28         this.loadFactor = DEFAULT_LOAD_FACTOR;
29         threshold = (int)(DEFAULT_INITIAL_CAPACITY  DEFAULT_LOAD_FACTOR);
30         table = new Entry[DEFAULT_INITIAL_CAPACITY];
31         init();
32     }

 我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为 16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于 initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。

  下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:


 1 public V put(K key, V value) {2         if (key == null) //如果键为null的话,调用putForNullKey(value)3             return putForNullKey(value);4         int hash = hash(key.hashCode());//根据键的hashCode计算hash码5         int i = indexFor(hash, table.length);6         for (Entry e = table[i]; e != null; e = e.next) { //处理冲突的,如果hash值相同,则在该位置用链表存储7             Object k;8             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值9                 V oldValue = e.value;
10                 e.value = value;
11                 e.recordAccess(this);
12                 return oldValue;
13             }
14         }
15
16         modCount++;
17         addEntry(hash, key, value, i);
18         return null;
19     }

  我们慢慢的来分析这个函数,第2和3行的作用就是处理key值为null的情况,我们看看putForNullKey(value)方法:

 1 private V putForNullKey(V value) {2         for (Entry e = table[0]; e != null; e = e.next) {3             if (e.key == null) {   //如果有key为null的对象存在,则覆盖掉4                 V oldValue = e.value;5                 e.value = value;6                 e.recordAccess(this);7                 return oldValue;8             }9         }
10         modCount++;
11         addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
12         return null;
13     }

 注意:如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。

  我们再回去看看put方法中第4行,它是通过key的hashCode值计算hash码,下面是计算hash码的函数:

1  //计算hash值的方法 通过键的hashCode来计算
2     static int hash(int h) {
3         // This function ensures that hashCodes that differ only by
4         // constant multiples at each bit position have a bounded
5         // number of collisions (approximately 8 at default load factor).
6         h ^= (h >>> 20) ^ (h >>> 12);
7         return h ^ (h >>> 7) ^ (h >>> 4);
8     }

  得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:

1     static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
2         return h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
3     }

  这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的n 次方,这是HashMap在速度上的优化。

当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

  这看上去很简单,其实比较有玄机的,我们举个例子来说明:

  假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

       h & (table.length-1)                     hash                             table.length-1

       8 & (15-1):                                 0100                   &              1110                   =                0100

       9 & (15-1):                                 0101                   &              1110                   =                0100

       ———————————————————————————————————————–

       8 & (16-1):                                 0100                   &              1111                   =                0100

       9 & (16-1):                                 0101                   &              1111                   =                0101

  

  从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

  

  上面说明了前面所说的HashMap容量 总是取2的指数次幂的原因。下面我们继续回到put方法里面,前面已经计算出索引的值了,看到第6到14行,如果数组中该索引的位置的链表已经存在key 相同的对象,则将其覆盖掉并返回原先的值。如果没有与key相同的键,则调用addEntry方法创建一个Entry对象,addEntry方法如下:

1 void addEntry(int hash, K key, V value, int bucketIndex) {
2         Entry e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3         table[bucketIndex] = new Entry<>(hash, key, value, e);
4         if (size++ >= threshold) //如果大于临界值就扩容
5             resize(2 * table.length); //以2的倍数扩容
6     }

  参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对 象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象 的next构成链表。

  第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。resize()方法如下:

 1     void resize(int newCapacity) {2         Entry[] oldTable = table;3         int oldCapacity = oldTable.length;4         if (oldCapacity == MAXIMUM_CAPACITY) {5             threshold = Integer.MAX_VALUE;6             return;7         }89         Entry[] newTable = new Entry[newCapacity];
10         transfer(newTable);//用来将原先table的元素全部移到newTable里面
11         table = newTable;  //再将newTable赋值给table
12         threshold = (int)(newCapacity * loadFactor);//重新计算临界值
13     }

  扩容是需要进行数组复制的,上面代码中第10行为复制数组,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

  

  总结:以前对HashMap和HashTable的区别总是要死记,而且容易忘记。分析完源码之后他们之前的区别都知道了,连细微的区别都能够清楚,而且是记忆深刻。所以研究一下源码,学习一下别人的设计思路可以学到很多东西的。

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

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

相关文章

Android成长日记-Android监听事件的方法

1. Button鼠标点击的监听事件 --setOnClickListener 2. CheckBox, ToggleButton , RadioGroup的改变事件 --setOnCheckedChangeListener Eg: 3. onPageChangeListener() ----用来监控ViewPager滑到第几页转载于:https://www.cnblogs.com/boy1025/p/4301956.html

XSS攻击(出现的原因、预防措施......)

验证XSS攻击重点不是去查找可输入哪些内容会出现什么样的bug就是测试XSS攻击&#xff0c;重点是了解它出现的原理&#xff0c;为什么会出现XSS攻击&#xff0c;导致一些问题出现&#xff1f;如何防御与解决XSS攻击&#xff1f;以下我将简单介绍以上提出的问题。 如何判定没有被…

一幅长文细学JavaScript(三)——DOM

文章目录3 JavaScript DOM3.1 DOM基本术语DOM模型及其作用文档对象模型节点节点的属性文档对象3.2 DOM文档操作3.2.1 查找网页元素3.2.2 获取元素内容新的策略——修改样式更好的策略——修改样式3.2.3 改变元素内容3.2.4 操作网页元素3.2.5 获取元素偏移offset和style的区别3.…

2016国内移动广告平台排行榜

为什么80%的码农都做不了架构师&#xff1f;>>> 移动营销的发展可追溯至第一台便携式手机的诞生&#xff0c;并随着移动终端的更新迭代和广告技术的发展创新&#xff1b;随着移动互联网的技术与商业模式的迅速发展&#xff0c;移动营销领域面临着种种创新与改革&am…

Planning Strategy 和Requirement type的思考

Planning Strategy 和 requirement type的联系 1. 当需要对一个material进行计划的时候 &#xff0c;我们会自然的考虑到plant的 production方式 ( MTO production or MTS production) ,如果能确定下来是MTO 还是MTS 的方式 &#xff0c; 那就可以确定了计划策略的选择 。 而计…

【摄影】田子坊

图片发自简书App图片发自简书App图片发自简书App图片发自简书App图片发自简书App图片发自简书App图片发自简书App图片发自简书App转载于:https://www.cnblogs.com/wangting888/p/9701627.html

selenium RC 环境配置

在网上搜索了许久&#xff0c;没找到有具体的配置&#xff0c;只是简单了写了几个步骤&#xff0c;自己琢磨了一下&#xff0c;于是&#xff0c;就想整理一篇文章&#xff0c;便于以后温习。 本文是参照官网的步骤进行了&#xff0c;当然了&#xff0c;也不完成相同。在这里我要…

一幅长文细学GaussDB(二)——数据库基础知识

文章目录2 数据库基础知识2.1 数据库管理简介数据库管理数据库管理工作范围对象管理制定数据库对象命名规范备份和恢复灾难恢复备份方式数据库安装数据库卸载数据库迁移数据库扩容例行维护工作2.2 数据库重要概念数据库和数据库实例数据库连接和会话数据库连接池模式表空间表数…

hive如何处理not in和in的问题

2019独角兽企业重金招聘Python工程师标准>>> 首先我们先创建两个表和测试数据。建表语句如下&#xff1a; create table table1(uid STRING, dayTimes BIGINT) PARTITIONED BY (dt STRING); create table table2(uid STRING, monTimes BIGINT) PARTITIONED BY (dt S…

一幅长文细学GaussDB(三)——SQL语法

文章目录3 SQL语法3.1 SQL语句概述SQL语句介绍SQL语句分类3.2 数据类型常用数据类型非常用数据类型3.3 系统函数概述数值计算函数字符处理函数时间日期函数类型转换函数系统信息函数3.4 操作符概述逻辑操作符比较操作符算术操作符测试操作符其他操作符3 SQL语法 华为GaussDB(f…

从网络获取数据显示到TableViewCell容易犯的错

2019独角兽企业重金招聘Python工程师标准>>> 昨晚第一次做用网络接口获取的数据&#xff0c;显示到自己的cell上&#xff0c;犯了很多的错&#xff0c; 总结如下&#xff1b; 1.数据源数组必须首先初始化&#xff0c;一般使用的是懒加载&#xff1b; 2.异步获取网络…

第二学期-第一次作业

1-1. 计算两数的和与差 1.设计思路 第一步&#xff1a;设出被调用函数 op1, op2, *psum, *pdiff &#xff0c;利用被调函数计算*psum的值和*pdiff的值&#xff1b; 第二步&#xff1a;代入到主函数就是计算a、b的和与差&#xff1b; 第三部&#xff1a;对所得到数值进行输出&a…

一幅长文细学华为MRS大数据开发(三)——Hive

文章目录3 HIVE3.1 Hive概述Hive简介Hive应用场景Hive与传统数据仓库比较Hive优点3.2 Hive功能及架构Hive运行流程Hive数据存储模型Hive数据存储模型-分区和分桶Hive数据存储模型-托管表和外部表Hive支持的函数3.3 Hive基本操作Hive使用DDL操作DML操作DQL操作3 HIVE Apache Hi…

Huffman树进行编码和译码

//编码 #include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<algorithm> #include<queue> #include<fstream> #include<map> using namespace std;typedef struct HuffmanNode{int w;//节点…

一幅长文细学JavaScript(五)——ES6-ES11新特性

5 ES版本 摘要 ES5的先天不足致使ES后续版本的发展&#xff0c;这也是前端人员绕不开的一个点。如果我们想要在工作和面试中轻松解决问题&#xff0c;那么了解ES6-ES11是必不可少的。 在本文中&#xff0c;我将采用一种更加通俗的方式来讲述这一块知识点&#xff0c;而不是照搬…

一幅长文细学Vue(一)——Webpack打包工具

1 项目开发工具 摘要 ​ 在本文中&#xff0c;我们会详细讨论webpack是如何打包发布项目&#xff0c;不过对于Vue来说&#xff0c;Vite可以做到和webpack一样的功能。 声明&#xff1a;如果想要看懂此文章&#xff0c;需具备node.js中npm的知识。 作者&#xff1a;来自ArimaMis…

I00005 打印直角三角形字符图案

曾经的计算机&#xff0c;没有显示屏&#xff0c;人们操作计算机时&#xff0c;用打印机记录执行的操作命令。 后来有了显示屏&#xff0c;不过最初的显示屏是字符频幕&#xff0c;输出只能是字符。 即使是今日&#xff0c;计算机已经进入多窗口图形界面时代&#xff0c;有时程…

动态数组使用

1 #include<stdio.h>2 #include<stdlib.h>3 4 int main()5 {6 int i;7 int n; //用于记录输入的整数的个数 8 int *p; //用于指向动态数组的存储空间 9 int sum0,average; //用于记录输入的整数的和与平均值 10 11 scanf("%d"…

Linux下安装Redis及搭建主从

Linux下安装Redis 首先在官网下载对应版本的redis包&#xff0c;这里本人使用的是redis-4.0.8.tar.gz。然后在服务器中存放redis包的路径下执行tar –vxf redis-4.0.8.tar.gz&#xff08;这里对应下载的包&#xff09;&#xff0c;解压redis后,cd 进入 redis-4.0.8&#xff08;…

图解安装CentOS 6.6

以下是在虚拟机上安装CentOS 6.6的过程。一、安装文件:CentOS-6.6-x86_64-bin-DVD.iso二、安装步骤# 虚拟机的配置这里省略&#xff0c;在百度上有很多帖子可以参考。开启虚拟机进入安装界面&#xff0c;如下图所示选择第一个选项&#xff0c;按Enter安装程序加载完后&#xff…