〖 作者:annegu 〗〖 大小:2K 〗〖 发布日期:2009-12-03 〗〖 浏览:1 〗 |
||||
*@author annegu *@date 2009-12-02 */ 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;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个元素要放到hashmap中,那么将hashmap的size设置为1024是一个比较好的选择,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 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有了一定的理解。 |
- 浏览: 96115 次
- 性别:
- 来自: 深圳
文章分类
- 全部博客 (82)
- Groovy & Grails (13)
- 面向对象数据库db4o (2)
- CoreJava (17)
- AOP (3)
- Spring (3)
- Hibernate (1)
- JAVA安全认证 (0)
- DDD (1)
- 职场 (1)
- IT杂谈 (4)
- 技术实践日志 (7)
- SOA (5)
- js (2)
- Python (3)
- cluster (2)
- load balance (2)
- distributed architeture (2)
- English (1)
- Wicket Tapestry (1)
- architecture (1)
- concurrenct (1)
- color UML (1)
- disruptor (2)
- linux (3)
- REST (1)
- sql injection (1)
- UML (1)
- eclipse 实用插件 (1)
- data structure & algorithm (2)
- lisp python (2)
- lisp (4)
- compiler (2)
- interpreter (4)
最新评论
-
zhaohuaxishiwzw:
老贴留名!
Tapestry and Wicket compared -
flyqantas:
经典的好文章。
UML类图与类的关系详解 -
myemptyname:
好文章,不管你懂不懂,我是懂了
闭包,懂不懂由你,反正我是懂了 -
GuolinLee:
看进来可能对我有点儿用,但是格式太差,太难看了放弃
java.util.concurrent 多线程框架 -
lonelybug:
在JiveJdon3.0中,值对象ForumState是被聚合 ...
实战DDD(Domain-Driven Design领域驱动设计:Evans DDD)
发表评论
-
海量数据处理系列——C语言下实现bitmap算法(转)
2015-03-01 20:40 607海量数据处理系列——C语言下实现bitmap算法 ... -
Preventing SQL Injection in Java
2013-07-26 10:17 1245Preventing SQL Injection in Ja ... -
java中静态代码块的用法 static用法详解
2013-07-21 10:07 833java中静态代码块的用法 static用法详解 ... -
String字符常量池和intern()的用法
2013-07-13 11:55 9计算机交流平台:计算 ... -
Java里的CompareAndSet(CAS)
2013-07-09 17:02 600Java里的CompareAndSet(CAS) A ... -
Tapestry and Wicket compared
2013-07-06 22:46 1462Skip to main content S ... -
awt 合并图片底色变黑问题,记录一下
2011-10-20 14:10 819原代码: image = ... -
java垃圾收集算法(转)
2010-04-29 00:20 8341.垃圾收集算法的核心思想 Java语言建立了垃圾收集机 ... -
各种垃圾回收算法的通俗解释(转)
2010-04-28 23:56 857引用计数( Reference Count ... -
单元测试利器 JUnit 4
2009-12-16 11:19 897[转帖] 单元测试利器 JUnit 4 引言 毋庸置疑,程 ... -
swing JTBALE获取最新数据方法
2009-12-02 11:23 928int row=table.getSelectedRow(); ... -
利用Doug Lea的并发包实现带超时机制的线程池
2008-11-12 12:55 1482利用Doug Lea的并发包实现带超时机制的线程池 j ... -
java.util.concurrent 多线程框架
2008-11-12 12:45 1218java.util.concurrent 多线程框架收藏 新一 ... -
构建高性能J2EE应用的五种核心策略
2008-07-20 17:33 825构建高性能J2EE应用的五 ... -
Java NIO原理和使用
2008-06-04 13:11 2484Java NIO原理和使用 板桥里人 2002/11/01 j ... -
能为你的程序锦上添花的五种程序组织形式
2007-09-08 20:16 1043能为你的程序锦上添花的五种程序组织形式 形式一." ... -
Java性能的优化
2007-09-08 19:13 995Java性能的优化 Java性能的优化 ...
相关推荐
indexFor 方法非常巧妙,它可以将 Hash 码映射到数组中的索引处,从而实现了键值对的快速查找。 HashMap 是一种复杂的数据结构,它的实现涉及到数组、链表和 Hash 码的计算。通过深入探讨 HashMap 的数据结构和 put...
它借鉴了稀疏矩阵的概念,通过巧妙地利用空间和计算资源,实现了对大量键值对的快速存取。 一、哈希映射与哈希集合基础 哈希映射(HashMap)和哈希集合(HashSet)都是基于哈希表的数据结构。哈希表通过哈希函数将...
Go语言的标准库`hash`包提供了多种哈希函数,如`hash/fnv`,可以根据需求选择合适的。 3. **扩容策略**:当HashMap达到一定负载因子时,需要进行扩容以保持效率。在无锁HashMap中,扩容操作需要创建一个新的更大的...
通过上述分析可以看出,`HashMap`的设计非常巧妙地结合了数组和链表/红黑树的优势,以提供高效的数据存储和检索功能。在设计过程中,通过对哈希值的精心处理和合理的扩容策略,有效地避免了哈希冲突的发生,并保证了...
深入理解Java中的HashMap,需关注其底层数据结构。HashMap本质上是一个数组与链表的组合,采用链表散列的方式组织数据。当插入数据时,首先计算键的哈希码,接着利用`index = hashcode % size`计算出数组索引,再将...
哈希表,如HashMap,利用哈希函数实现快速存取,是处理大量数据的有效手段。Bloom Filter和Bitmap则是节省空间的过滤器,用于判断元素是否存在,适用于空间有限的环境。 3. **排序算法**: 对于需要排序的数据,堆...
在哈希表的设计中,HashMap通过巧妙的哈希算法和数据结构优化,能够有效地处理大量的键值对插入和查找操作。哈希冲突的处理是关键,通过链表和红黑树的结合,HashMap在保持高效率的同时,还能保证数据的合理分布,...
通过以上分析,我们可以看到ConcurrentHashMap如何通过锁分段技术来解决HashMap在并发环境下的线程安全问题,并通过巧妙的设计减少线程间锁的竞争,从而提升性能。因此,在设计需要高并发性能的程序时,...