深入理解HashMap(原理,查找,扩容)

面试的时候闻到了Hashmap的扩容机制,之前只看到了Hasmap的实现机制,补一下基础知识,讲的非常好

原文链接:

http://www.iteye.com/topic/539465

Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论。 

1、hashmap的数据结构 
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外。Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。 

 

从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。我们来看看java代码:

 

/** * The table, resized as necessary. Length MUST Always be a power of two. *  FIXME 这里需要注意这句话,至于原因后面会讲到 */  transient Entry[] table;  

 

static class Entry<K,V> implements Map.Entry<K,V> {  final K key;  V value;  final int hash;  Entry<K,V> next;  
..........  
}  

上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。 
     当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~ 

2、hash算法 
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 

所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

 

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



首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 

 


          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 
          说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。 

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

// Find a power of 2 >= initialCapacity  int capacity = 1;  while (capacity < initialCapacity)   capacity <<= 1;  



总结: 
        本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。


3、hashmap的resize 

       当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 

         那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。 


4、key的hashcode与equals方法改写 
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。 

Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。
在改写equals方法的时候,需要满足以下三点: 
(1) 自反性:就是说a.equals(a)必须为true。 
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。 
(3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。 
通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。

 

总结: 
        本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。

转发:https://www.cnblogs.com/Jacck/p/8034558.html

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

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

相关文章

uml 类图整理

1.五分钟读懂UML类图 http://www.cnblogs.com/shindo/p/5579191.html 2.UML类关系&#xff08;依赖&#xff0c;关联&#xff0c;聚合&#xff0c;组合的区别&#xff09; https://www.jianshu.com/p/eefa0b5b4922 2.1 关联 1、关联关系 关联关系又可进一步分为单向关联、…

web控件开发系列(四) 自定义控件属性(下)

控件在WEB开发时经常要用到&#xff0c;虽然有部分已经存在工具箱里&#xff0c;但有时总需要根据自己的要求&#xff0c;开发一些合适自己的控件。接上一节,已经说过了控件的属性, 例如&#xff0c;我们需要一组属性的集合时&#xff0c;这时我们需要用到的就是复杂属性了&…

23设计模式学习

1.什么设计模式 1.1模式是解决相似问题的核心1.2设计模式软件设计过程中解决一类问题的方案总结来说&#xff1a;设计模式 &#xff1a;软件设计过程中解决一类问题的一种方案&#xff1b; 2.什么是面向对象的设计模式 面向对象设计模式描述了面向对象设计过程中、特定场景下…

移植u-boot-2012.04.01到jz2440开发板

今天我给大家分享一下如何移植一个纯净的uboot到jz2440开发大版&#xff0c;通过记录学习分享&#xff0c;与大家一起进步&#xff01;&#xff01;&#xff01; 1.首先我们在uboot官网下载u-boot-2012.04.01.tar.bz2&#xff0c;建立source insight工程。将下载好的uboot通过…

温习了一下java线程状态方面的知识总结一

好长时间没有复习线程状态这块&#xff0c;读并发编程实战正好看着这块&#xff0c;顺便复习一下&#xff1a; 1.线程的五种状态&#xff1a; 2.线程五种状态的转换图&#xff1a; wait()会立刻释放synchronized(obj)中的锁以便其它线程可以执行obj.notify 但是notify() 不会立…

WinForm:进度条的实现(异步)

在WinForm中经常遇到一些费时的操作界面&#xff0c;比如统计某个磁盘分区的文件夹或者文件数目&#xff0c;如果分区很大或者文件过多的话&#xff0c;处理不好就会造成“假死”的情况&#xff0c;或者报“线程间操作无效”的异常&#xff0c;为了解决这个问题&#xff0c;可以…

移植u-boot-2012.04.01到jz2440开发板之修改代码支持NAND启动

上一篇文章我们已经修改了uboot源码使其能够正常打印输出了&#xff0c;但是输出停到了nand启动那里&#xff0c;所以这篇文章就来记录如何修改源码使uboot支持NAND启动。 原来的代码在链接时加了”-pie”选项, 使得u-boot.bin里多了”(.rel)”, “*(.dynsym)”使得程序非常大…

【C++深度剖析教程4】C++的二阶构造模式

今天学习的是C中的二阶构造模式&#xff0c;二阶构造模式只是设计模式中的简单的模式&#xff0c;是一种软件设计的方法&#xff0c;并没有我们想象的那么高深&#xff0c;设计模式也是一样&#xff0c;只不过是一系列的设计方法&#xff0c;只要我们懂得了原理&#xff0c;那么…

【C++深度剖析教程5】C++中类的静态成员函数

学习交流加&#xff08;可免费帮忙下载CSDN资源&#xff09;&#xff1a;个人微信&#xff1a; liu1126137994学习交流资源分享qq群1&#xff08;已满&#xff09;&#xff1a; 962535112学习交流资源分享qq群2&#xff08;已满&#xff09;&#xff1a; 780902027学习交流资源…

eclipse 工程中使用引入maven项目遇到maven-resources-plugin:2.6 找不到

1.开始eclipse 配置本地库 首先是从maven 官网下载maven 组件 其次是配置 maven 环境和java 配置jdk 类似这里就不做介绍了 配置完环境后修改\apache-maven-3.3.9\conf\settings.xml 中的<localRepository> 设置本地仓库 然后配置eclipse 下面图中执行较为重要&#xff…

移植uboot之修改代码支持NorFlash记录

学习交流加 个人qq&#xff1a; 1126137994个人微信&#xff1a; liu1126137994学习交流资源分享qq群&#xff1a; 962535112 今天我们的任务是修改uboot源码支持NorFlash。 上两篇关于uboot移植的文章&#xff0c;我们修改了uboot源代码&#xff0c;支持了串口的输出&#xff…

移植uboot之修改代码支持NorFlash记录续集

接着上一篇文章写的内容&#xff08;上一篇文章链接&#xff1a;移植uboot之修改代码支持NORFLASH&#xff09;&#xff0c;上一篇结尾测试flash的擦除读写功能&#xff0c;结果无法写flash&#xff0c;卡在了这里&#xff1a; 前面已经擦除成功&#xff0c;这里写内容写不进…

C#开发终端式短信的原理和方法

本文示例源代码或素材下载 简介 没发过短信的年轻人肯定是属于那种受保护的稀有动物&#xff0c;通讯发达的今天短信已经成为人们交流的重要手段&#xff0c;其中也蕴含着巨大的市场和经济利益&#xff0c;掌握短信技术的人才也受到各大公司的追捧是目前职场上耀眼的明星。本文…

移植uboot之修改代码支持NorFlash记录续集二

先说一个事&#xff1a;我会在最后把移植好的uboot&#xff0c;内核&#xff0c;分别做一个补丁文件&#xff0c;以后如果用到相同的uboot以及内核都可以直接下载我这个补丁进行打补丁操作就可以直接用~ 上一个移植uboot续集&#xff0c;我们解决了无法写flash的问题&#xff…

Mysql function(函数)

1.mysql 拼接函数 1. 1CONCAT(string1,string2,…) 说明 : string1,string2代表字符串,concat函数在连接字符串的时候&#xff0c;只要其中一个是NULL,那么将返回NULL 1.2 CONCAT_WS(separator,str1,str2,...)说明 : string1,string2代表字符串,concat_ws 代表 conca…

【C++深度剖析教程6】C++之友元

这几天在复习数学考试&#xff0c;都没有学C&#xff0c;今天抽空来学一点。 什么是友元&#xff1f; 友元是C中的一种关系友元发生在函数与类之间或者类与类之间友元关系是单向的&#xff0c;不能传递 在具体讲解友元的性质之前&#xff0c;我们先来看看一个程序&#xff…

call stack and stack buffer overflow

http://en.wikipedia.org/wiki/Call_stack http://en.wikipedia.org/wiki/Stack_buffer_overflow Stack_buffer_overflow里提到的frame pointer 的位置不一样&#xff0c;不同的系统实现应该是不一样的。 运行时的栈是从高地址向低地址分配的&#xff0c;堆是从低地址向高地址…

【C++深度剖析教程8】C++的操作符重载的概念

之前学习了类的函数重载的概念&#xff0c;今天学习操作符重载的概念。在这之前我们先看一个例子&#xff1a; 上面是一个复数的加法&#xff0c;a为复数的实部&#xff0c;b为复数的虚部&#xff0c;在main函数里我想实现复数c1与c2的加法。很显然&#xff0c;正常的号操作符…

大数据开发者应该知道的分布式系统 CAP 理论

无论你是一个系统架构师&#xff0c;还是一个普通开发&#xff0c;当你开发或者设计一个分布式系统的时候&#xff0c;CAP理论是无论如何也绕不过去的。本文就来介绍一下到底什么是CAP理论&#xff0c;如何证明CAP理论&#xff0c;以及CAP的权衡问题。 CAP理论概述 CAP理论&a…