• 问题背景:现要将10个人的电话号码记录在一个本子上,以便以后某天有急事能快速找到他们。
  • 方案一:直接在本子上写上10人对应的名字和电话号码。如:张三 111 ;李四 222;王五 333 ......
  • 方案二:在本子上以姓名的首字母划分,并记录电话号码。如:Z-张三 111  ; L-李四 222;W-王五 333

ok,现在我要找张三的电话号码了,来看看是怎么找的

  • 方案一:把所有的记录都比较一次,如果张三在最前面,我一次就能找到,如果张三在最后,那我要找10次,如果我要存100个、1000个、10000个人的电话号码呢?那我还不气得把本子撕咯?
  • 方案二: 直接去Z分类里边儿找,一发入魂。但是如果出现很多姓名的首字母是Z的人,比如Z-赵六 444、Z-周七 555 ,当我存的电话多起来,我一样需要找好多次。

什么是Hash?

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。ok,这样的定义可能比较抽象。咱们结合上面提到的方案二的例子一句一句解释。

首先,在方案二中,人的姓名就是一个任意长度的输入。

然后,在方案二中,我是按照姓名的首字母的大写去进行一个分类,这个划分的方式我们可以看成是散列算法。

最后,固定长度的输出,即我把所有的电话全部存入了26个分类中。那么这26个字母就是散列值。

什么是Hash碰撞?

在上面的例子中,Z分类可能出现赵六、周七、钟八等等首字母为Z的姓名吧,我们说,这就发生了Hash碰撞。

ok,方案二中我可以把姓名按照首字母划分,那我当然也可以按照百家姓去划分对不?那这样一来,我就会去张姓分类去找张三,赵姓分类里去找赵六,周姓分类里去找周七对吧。虽然这种划分方法能减少Hash碰撞的频率,提高查询的速度(时间),但是却需要比首字母划分更多的存储位置(空间)。由此可见,一个好的散列算法,是能够在时间和空间之间寻找平衡的。

java中HashMap是如何实现的?

大家都知道,HashMap底层使用的是数组加链表的数据结构实现的。

java中HashMap是如何使用的?

首先,得new一个HashMap对象不是?很多人都是用默认的无参构造器来创建HashMap对象的。ok咱们看一下jdk1.6中的源码

 public HashMap() {//加载因子,默认0.75this.loadFactor = DEFAULT_LOAD_FACTOR;//当HashMap中存储的键值对数超过threshold,会引起扩容//默认threshold为 默认数组长度*默认加载因子 即16*0.75threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//创建entry数组,默认值16table = new Entry[DEFAULT_INITIAL_CAPACITY];init();}

注释写的很清楚,默认创建一个大小为16的entry数组,当HashMap里存的entry个数达到12,就会发生扩容,这当然会影响HashMap的性能不是?所以,推荐咱们使用HashMap提供的另外一个构造器。

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

啊咧,调用的是HashMap提供的另一个构造器,传入的加载因子为默认的0.75,继续看源码

 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);// Find a power of 2 >= initialCapacityint capacity = 1;while (capacity < initialCapacity)capacity <<= 1;this.loadFactor = loadFactor;threshold = (int)(capacity * loadFactor);table = new Entry[capacity];init();}

假如现在我要往HashMap里存12个键值对,那么经过代码段1:

while (capacity < initialCapacity)
                     capacity <<= 1;

计算得出的capacity为16,然后再经过代码段2:

threshold = (int)(capacity * loadFactor);
            table = new Entry[capacity];

创建了一个threshold为12,数组大小为16的entry数组。刚好达到我们的要求。

但是,如果我想要往HashMap里存16个键值对,那么经过代码段1,计算得出的capacity依然为16,然后经过代码段2创建了一个threshold为12,数组大小为16的entry数组。ok那当我往这个HashMap里存入第13个键值对的时候,就会引起扩容(因为threshold =12),扩容操作如果说基数小的话,对性能的影响可能不大,但随着基数的变大,对性能的影响也会随之变大。

所以,推荐在使用这个构造器的时候,我们先计算一下,设你想存x个键值对,那么你应该传入的参数为y。默认加载因子为0.75,那么先用x除以0.75,然后再计算出比x/0.75大的最小的2的幂。

ok,带入计算公式,我要存12个键值对,x=12,则x/0.75=16,因为2^4=16,所以y=16

我要存15个键值对,x=15,x/0.75=20,那么大于20的最小的2的幂为32,y=32,我们传入参数32。这样就不会发生扩容。

当然,理想很丰满,现实很骨感,实际开发中,我们基本上是无法确定我们要存入多少个键值对的,所以,管它呢,咱大概估一个值,传入就行了,但是最好不要使用默认的无参的构造器,原因很简单,假设我们现在要大概要存100个建值对,使用无参构造器,在存入16*0.75=12个建值对 的时候会发生第一次扩容,扩容为16*2=32,然后在插入32*0.75=28个建值对的时候发生第二次扩容,扩容为32*2=64,依次类推。但是我们调用new HashMap(100),那么,只会在我们插入第128*0.75=96个建值对的时候发生一次扩容。对性能的提升由此可见一斑。所以上面哔哔了那么多,其实还是没什么卵用的,哈哈哈哈。

然后,当我们创建好HashMap的实例后,我们会调用它的put方法插入键值对,ok,上源码:

 public V put(K key, V value) {//step 1if (key == null)return putForNullKey(value);//step 2int hash = hash(key.hashCode());//step 3int i = indexFor(hash, table.length);//step 4for (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++;//step 5addEntry(hash, key, value, i);return null;}

step1:如果传入的key为空,那么返回空键对应的值(HashMap允许一个null键)。

step2:把key的hashcode经过一个hash算法,得到一个hash值。(这个方法咱不讨论,那是数学家研究的事)

step3:根据上一步得到的hash值定位数组的下标。

step4:上一步定位到数组的下标的位置如果有元素,取第一个元素,判断他们的hash值是否相同,如果相同,再判断两个对象是否相等。如果hash值不同,或hash值相同,却不相等,判断第二个元素。直到判断完所有的元素。

step5:把当前元素作为数组下标位置的第一个元素,把之前的元素设为新插入元素的next节点,判断是否超过threshold,超过则扩容(addEntry逻辑说明)。

ok,对比方案二的例子,我们把key看成是一个人,key的hashCode看成是人的姓名,value看成是电话号码。

假如我现在要存张三的电话号码,那么,对应的:

step2:把姓名按照首字母划分,得到hash值为Z。

step3:找到Z所在的位置。

step4:判断Z位置处第一个元素(假设Z位置处只有一个赵六),赵六的hash值也为Z,所以,继续比较到底是不是我要找de

的张三,ok,不是,判断Z位置处不存在第二个元素,执行step5。

step5:在Z位置处插入张三和他的电话号码,把赵六移到他的后面。

这里需要注意的是step3中,根据hash值定位数组下标后,这个下标处的元素的hash值是不一定相等的。为什么?看step3处的方法的源码:

static int indexFor(int h, int length) {return h & (length-1);}

ok,假如传进来的hash值为1,那么返回的下标是1 & 15

java中int类型是32位那么 1 & 15 如下:

0000 0000 0000 0000 0000 0000  0000 0001          -1的二进制表示

0000 0000 0000 0000 0000 0000  0000 1111           -15的二进制表示

按位与操作只有两个数都是1才会返回1,否则返回0。

那么显然,1 & 15  得1,即返回的数组下标为1。

咱再来看看如果传进来的hash值是17,会怎样。

ok,若传进来的hash值为17,则返回 17  &  15

0000 0000 0000 0000 0000 0000  0001 0001          -17的二进制表示

0000 0000 0000 0000 0000 0000  0000 1111           -15的二进制表示

对的整整齐齐,明明白白,一眼就能看出17  &  15  =  1

综上,不同的hash值通过下标是可以定位到相同的数组位桶的。

这里的这个定位算法和数组的长度也是有关系的,为什么HashMap的数组长度是2的幂?

咱假设这里不是二的幂,看看会出现啥情况。

若数组的长度是10,indexFor方法返回的是  h  & 9  则

0000 0000 0000 0000 0000 0000  0000 1001              -9的二进制表示

看到最后4位,中间的两个0了么?无论你传进来的hash值是多少,中间那两个0相与,永远返回0。这样一来,就大大的增加了hash碰撞的概率。

所以这时我们再来看看构造器中这一段代码:

while (capacity < initialCapacity)
                        capacity <<= 1;

它能保证初始化的数组的大小一定是2的幂。以便在定位数组位桶的下标时使用。

最后,我们使用get方法,从HashMap中找到我们想要的键值对。来看看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;}

这个就不用多说了吧。先hash,再定位,再比较hash值,再equals。。。

HashMap使用的一些建议

一般而言,HashMap的key是要用类似String、Integer这样的不可变类的。夺西碟?

咱们看看String类的hashCode方法:

 //成员变量hash,初始值为0.private int hash;public int hashCode() {int h = hash;int len = count;//如果h==0代表第一次调用此方法,调用完之后,把计算出来的值付给成员变量hash//第二次调用此方法时,h == 0 返回 false,直接return h。if (h == 0 && len > 0) {int off = offset;char val[] = value;for (int i = 0; i < len; i++) {h = 31*h + val[off++];}hash = h;}return h;}

使用String当做key,只会在第一次调用hashCode方法时,计算出hash值,以后的调用直接使用已缓存的hash值即可。这样设计,又能大大的提升HashMap的性能。

所以,推荐使用这种不可变类。

当然,如果你想用自定义的类当做key,那么,请参考String类的设计,而且,这个你自定义的类必须是不可变得,至少参与hashCode计算的字段是不可变的,不然,会出现存进去的键值对和取出来的键值对不一致。

写在最后

这是我的第一篇技术博客,花了将近6个小时才写完,虽然哔哔了一大堆,排版也很乱,但是收获是满满的。希望以后能一直坚持写下去,主动学习,提高自己的竞争力。加油。

HashMap实现原理(超详细)相关推荐

  1. 计算机网络之交换机的工作原理---超详细解析,谁都看得懂!!

    在了解交换机的工作原理之前,我们先要了解几个概念. 一.相关概念  1.OSI七层模型是哪七层? 自上而下分别是: 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 交换机工作在数据链路层, ...

  2. 【算法】动态规划+“背包九讲”原理超详细讲解+常见dp问题(9种)总结

    目录 一.动态规划(DP) 二.背包九讲 (1)完全背包 P1616 疯狂的采药(完全背包) (2)01背包 滚动数组 一维数组 P1048 采药(01背包) 01背包表格图示 (3)多重背包 整数拆 ...

  3. iic总线从机仲裁_IIC协议底层原理超详细解析!示波器,逻辑分析仪多图预警

    1. 协议基础 1.1.     协议简介 IIC-BUS(Inter-IntegratedCircuit Bus)最早是由PHilip半导体(现在被NXP收购)于1982年开发.主要是用来方便微控制 ...

  4. 增量式编码器工作原理超详细图解

    旋转编码器是由光栅盘(又叫分度码盘)和光电检测装置(又叫接收器)组成.光栅盘是在一定直径的圆板上等分地开通若干个长方形孔.由于光栅盘与电机同轴,电机旋转时,光栅盘与电机同速旋转,发光二极管垂直照射光栅 ...

  5. 支持向量机(SVM)优化算法原理超详细证明过程,几何的角度证明互补松弛条件

    本文将初学者在硬间隔优化这一部分会遇到的问题基本都进行了说明和推导,包括为什么要取max以及互补松弛条件到底是什么,支持向量到底体现在哪里. 但是本文并没有引入说明支持向量的优化问题是怎么引出的.这里 ...

  6. JVM原理-超详细总结

    JVM概念 JVM是java的核心和基础在java编译器和os平台之间的虚拟处理器.它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序.java编译 ...

  7. Kafka原理--超详细(学习笔记)

    一.概念理解 ​ Kafka 是最初由Linkedin 公司开发,**是一个分布式.支持分区的(patition).多副本的(replica).**基于zookeeper协调的分布式消息系统,它的最大 ...

  8. SPI原理超详细讲解---值得一看

    文章目录 什么是SPI SPI主从模式 SPI信号线 SPI设备选择 SPI数据发送接收 SPI通信的四种模式 SPI的通信协议 SPI的三种模式 SPI原理图连接 STM32中SPI初始化配置 什么 ...

  9. 【GAMES-202实时渲染】1、软阴影01(Shadow Mapping、Peter Panning、PCSS原理超详细)

    Lecture3 Real-Time shadows1 1 Shadow Mapping回顾 2 Shadow Mapping缺点及解决方案 2.1 自遮挡现象 解决方案1 定义一个bias 解决方案 ...

  10. 汉诺塔原理超详细讲解+变式例题

    目录 一.汉诺塔详解 1.详解 2.完整代码 二.汉诺塔公式:ans=2^n^-1 1.例题:P1760 通天之汉诺塔 三.变式1:牛牛的汉诺塔 四.变式2:P4285 [SHOI2008]汉诺塔 A ...

最新文章

  1. SSO单点登录基于CAS架构封装 Memcached 实例
  2. php 企业微信指令回调借款_企业微信外部联系人回调事件
  3. 受益一生的15个学习习惯
  4. 13个球一个天平,现知道只有一个和其它的重量不同,怎样称才能用三次就找到那个球?...
  5. Java学习—— for循环
  6. 做海外运营?这125条核心数据你需要Get
  7. 计算机操作员考评标准,计算机操作员职业技能鉴定标准.doc
  8. 黑苹果睡眠重启后死机_iPhone卡屏、死机、黑屏?| 学会这招,秒解!
  9. Ubuntu禁用网卡步骤(重启依然生效)
  10. Win 7 安装VMware Workstation Pro 14出现 “Intel VT-x禁用”问题以及“无法连接 MKS: 套接字连接尝试次数太多;正在放弃”问题的实质性解决
  11. this.value和$(this).val()的区别
  12. 【优化求解】基于matlab蚁群算法求解函数极值问题【含Matlab源码 1201期】
  13. 东芝抢先一步,推出了全球首款16TB容量的硬盘MG08系列
  14. S7-1200/1500获取本地IP地址(不使用库)
  15. python永久删除文件_Python彻底删除文件夹及其子文件方式
  16. Alpine 安装 MySQL
  17. Rtsp服务搭建(Ffmpeg+Node.js+jsmpeg网络视频服务器)
  18. bogomips 骁龙820_中兴这款骁龙820手机堪称经典,如今跌幅近千元
  19. C#使用post方式调用接口获取Token及调用网页地址
  20. 【文末抽书】Java设计模式--单例模式

热门文章

  1. access2013数据库实验笔记_二级Access数据库备考笔记之使用查询向导
  2. swift之switch介绍
  3. 微信小程序(1)——注册开发者账号、安装开发工具
  4. kali linux 网卡配置ip地址,Kali Linux常用服务配置教程获取IP地址
  5. SQL Server 常用更新语句,用B表数据作为条件或数据源更新A表数据
  6. 一般纳税人与小规模纳税人是对增值税纳税人区别
  7. DSSM、YoutubeDNN-推荐系统小结
  8. 3G技术必将加速中小企业信息化进程
  9. 安全测试 : 潮点视频(shipin520.com)网站短信接口安全测试,你的网站安全吗?这一漏洞可能让你瞬间损失过万。
  10. 第二章 4 色彩范围抠图以及 案例