`
itace
  • 浏览: 172218 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

Hashmap深入解析【转】

 
阅读更多

简单概括:key=>key.hashcode=>位运算出一个hash值=>buckedIndex=>相等,equals比较value=>真正的键值对pair是buckedIndex=value。

 

转载:http://blog.csdn.net/ghsau/article/details/16843543

http://blog.csdn.net/ghsau/article/details/16890151

 

 HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发。在很多Java的笔试题中也会被问到,最常见的,“HashMap和HashTable有什么区别?”,这也不是三言两语能说清楚的,这种笔试题就是考察你来笔试之前有没有复习功课,随便来个快餐式的复习就能给出简单的答案。
       HashMap计划写两篇文章,一篇是HashMap工作原理,也就是本文,另一篇是多线程下的HashMap会引发的问题。这一年文章写的有点少,工作上很忙,自己业余时间也做点东西,就把博客的时间占用了,以前是力保一周一篇文章,有点给自己任务的意思,搞的自己很累,文章质量也不高,有时候写技术文章也是需要灵感的,为了举一个例子可能要绞尽脑汁,为了一段代码可能要验证好多次,现在想通了,有灵感再写,需要一定的积累,才能把自己了解的知识点总结归纳成文章。
       言归正传,了解HashMap之前,我们需要知道Object类的两个方法hashCode和equals,我们先来看一下这两个方法的默认实现:

 

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. /** JNI,调用底层其它语言实现 */  
  2. public native int hashCode();  
  3.   
  4. /** 默认同==,直接比较对象 */  
  5. public boolean equals(Object obj) {  
  6.     return (this == obj);  
  7. }  

       equals方法我们太熟悉了,我们经常用于字符串比较,String类中重写了equals方法,比较的是字符串值,看一下源码实现:

 

 

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. public boolean equals(Object anObject) {  
  2.     if (this == anObject) {  
  3.         return true;  
  4.     }  
  5.     if (anObject instanceof String) {  
  6.         String anotherString = (String) anObject;  
  7.         int n = value.length;  
  8.         if (n == anotherString.value.length) {  
  9.             char v1[] = value;  
  10.             char v2[] = anotherString.value;  
  11.             int i = 0;  
  12.             // 逐个判断字符是否相等  
  13.             while (n-- != 0) {  
  14.                 if (v1[i] != v2[i])  
  15.                         return false;  
  16.                 i++;  
  17.             }  
  18.             return true;  
  19.         }  
  20.     }  
  21.     return false;  
  22. }  

       重写equals要满足几个条件:

 

 

  • 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。 
  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。 
  • 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。 
  • 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。 
  • 对于任何非空引用值 x,x.equals(null) 都应返回 false。 
       Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。 当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
       下面来说说hashCode方法,这个方法我们平时通常是用不到的,它是为哈希家族的集合类框架(HashMap、HashSet、HashTable)提供服务,hashCode 的常规协定是:
  • 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 
  • 以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
       当我们看到实现这两个方法有这么多要求时,立刻凌乱了,幸好有IDE来帮助我们,Eclipse中可以通过快捷键alt+shift+s调出快捷菜单,选择Generate hashCode() and equals(),根据业务需求,勾选需要生成的属性,确定之后,这两个方法就生成好了,我们通常需要在JavaBean对象中重写这两个方法。
       好了,这两个方法介绍完之后,我们回到HashMap。HashMap是最常用的集合类框架之一,它实现了Map接口,所以存储的元素也是键值对映射的结构,并允许使用null值和null键,其内元素是无序的,如果要保证有序,可以使用LinkedHashMap。HashMap是线程不安全的,下篇文章会讨论。HashMap的类结构如下:

java.util 
类 HashMap<K,V>

java.lang.Object
  
继承者
java.util.AbstractMap<K,V>
      
继承者
java.util.HashMap<K,V>
所有已实现的接口:
Serializable,Cloneable,Map<K,V>
直接已知子类:
LinkedHashMap,PrinterStateReasons
       HashMap中我们最长用的就是put(K, V)和get(K)。我们都知道,HashMap的K值是唯一的,那如何保证唯一性呢?我们首先想到的是用equals比较,没错,这样可以实现,但随着内部元素的增多,put和get的效率将越来越低,这里的时间复杂度是O(n),假如有1000个元素,put时需要比较1000次。实际上,HashMap很少会用到equals方法,因为其内通过一个哈希表管理所有元素,哈希是通过hash单词音译过来的,也可以称为散列表,哈希算法可以快速的存取元素,当我们调用put存值时,HashMap首先会调用K的hashCode方法,获取哈希码,通过哈希码快速找到某个存放位置,这个位置可以被称之为bucketIndex,通过上面所述hashCode的协定可以知道,如果hashCode不同,equals一定为false,如果hashCode相同,equals不一定为true。所以理论上,hashCode可能存在冲突的情况,有个专业名词叫碰撞,当碰撞发生时,计算出的bucketIndex也是相同的,这时会取到bucketIndex位置已存储的元素,最终通过equals来比较,equals方法就是哈希码碰撞时才会执行的方法,所以前面说HashMap很少会用到equals。HashMap通过hashCode和equals最终判断出K是否已存在,如果已存在,则使用新V值替换旧V值,并返回旧V值,如果不存在 ,则存放新的键值对<K, V>到bucketIndex位置。文字描述有些乱,通过下面的流程图来梳理一下整个put过程。
       现在我们知道,执行put方法后,最终HashMap的存储结构会有这三种情况,情形3是最少发生的,哈希码发生碰撞属于小概率事件。到目前为止,我们了解了两件事:
  • HashMap通过键的hashCode来快速的存取元素。
  • 当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。
       来鉴赏一下HashMap中put方法源码:
[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. public V put(K key, V value) {  
  2.     // 处理key为null,HashMap允许key和value为null  
  3.     if (key == null)  
  4.         return putForNullKey(value);  
  5.     // 得到key的哈希码  
  6.     int hash = hash(key);  
  7.     // 通过哈希码计算出bucketIndex  
  8.     int i = indexFor(hash, table.length);  
  9.     // 取出bucketIndex位置上的元素,并循环单链表,判断key是否已存在  
  10.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  11.         Object k;  
  12.         // 哈希码相同并且对象相同时  
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             // 新值替换旧值,并返回旧值  
  15.             V oldValue = e.value;  
  16.             e.value = value;  
  17.             e.recordAccess(this);  
  18.             return oldValue;  
  19.         }  
  20.     }  
  21.   
  22.     // key不存在时,加入新元素  
  23.     modCount++;  
  24.     addEntry(hash, key, value, i);  
  25.     return null;  
  26. }  

 

  HashMap有两个参数影响其性能:初始容量加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
       HashMap中定义的成员变量如下:

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * The default initial capacity - MUST be a power of two. 
  3.  */  
  4. static final int DEFAULT_INITIAL_CAPACITY = 16;// 默认初始容量为16,必须为2的幂  
  5.   
  6. /** 
  7.  * The maximum capacity, used if a higher value is implicitly specified 
  8.  * by either of the constructors with arguments. 
  9.  * MUST be a power of two <= 1<<30. 
  10.  */  
  11. static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量为2的30次方  
  12.   
  13. /** 
  14.  * The load factor used when none specified in constructor. 
  15.  */  
  16. static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认加载因子0.75  
  17.   
  18. /** 
  19.  * The table, resized as necessary. Length MUST Always be a power of two. 
  20.  */  
  21. transient Entry<K,V>[] table;// Entry数组,哈希表,长度必须为2的幂  
  22.   
  23. /** 
  24.  * The number of key-value mappings contained in this map. 
  25.  */  
  26. transient int size;// 已存元素的个数  
  27.   
  28. /** 
  29.  * The next size value at which to resize (capacity * load factor). 
  30.  * @serial 
  31.  */  
  32. int threshold;// 下次扩容的临界值,size>=threshold就会扩容  
  33.   
  34.   
  35. /** 
  36.  * The load factor for the hash table. 
  37.  * 
  38.  * @serial 
  39.  */  
  40. final float loadFactor;// 加载因子  

       我们看字段名称大概就能知道其含义,看Doc描述就能知道其详细要求,这也是我们日常编码中特别需要注意的地方,不要写让别人看不懂的代码,除非你写的代码是一次性的。需要注意的是,HashMap中的容量MUST be a power of two,翻译过来就是必须为2的幂,这里的原因稍后再说。再来看一下HashMap初始化,HashMap一共重载了4个构造方法,分别为:

构造方法摘要
HashMap()
          构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
HashMap(int initialCapacity)
          构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
HashMap(int initialCapacity, float loadFactor)
          构造一个带指定初始容量和加载因子的空 HashMap
HashMap(Map<? extendsK,? extendsV> m)
          构造一个映射关系与指定 Map 相同的 HashMap

       看一下第三个构造方法源码,其它构造方法最终调用的都是它。

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  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.     // 这里需要注意一下  
  14.     int capacity = 1;  
  15.     while (capacity < initialCapacity)  
  16.         capacity <<= 1;  
  17.   
  18.     // 设置加载因子  
  19.     this.loadFactor = loadFactor;  
  20.     // 设置下次扩容临界值  
  21.     threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  
  22.     // 初始化哈希表  
  23.     table = new Entry[capacity];  
  24.     useAltHashing = sun.misc.VM.isBooted() &&  
  25.             (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
  26.     init();  
  27. }  

       我们在日常做底层开发时,必须要严格控制入参,可以参考一下Java源码及各种开源项目源码,如果参数不合法,适时的抛出一些运行时异常,最后到应用层捕获。看第14-16行代码,这里做了一个移位运算,保证了初始容量一定为2的幂,假如你传的是5,那么最终的初始容量为8。源码中的位运算随处可见啊=。=!
       到现在为止,我们有一个很强烈的问题,为什么HashMap容量一定要为2的幂呢?HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。那如何计算才会分布最均匀呢?我们首先想到的就是%运算,哈希值%容量=bucketIndex,SUN的大师们是否也是如此做的呢?我们阅读一下这段源码:

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * Returns index for hash code h. 
  3.  */  
  4. static int indexFor(int h, int length) {  
  5.     return h & (length-1);  
  6. }  

       又是位运算,高帅富啊!这里h是通过K的hashCode最终计算出来的哈希值,并不是hashCode本身,而是在hashCode之上又经过一层运算的hash值,length是目前容量。这块的处理很有玄机,与容量一定为2的幂环环相扣,当容量一定是2^n时,h & (length - 1) == h % length,它俩是等价不等效的,位运算效率非常高,实际开发中,很多的数值运算以及逻辑判断都可以转换成位运算,但是位运算通常是难以理解的,因为其本身就是给电脑运算的,运算的是二进制,而不是给人类运算的,人类运算的是十进制,这也是位运算在普遍的开发者中间不太流行的原因(门槛太高)。这个等式实际上可以推理出来,2^n转换成二进制就是1+n个0,减1之后就是0+n个1,如16 -> 10000,15 -> 01111,那根据&位运算的规则,都为1(真)时,才为1,那0≤运算后的结果≤15,假设h <= 15,那么运算后的结果就是h本身,h >15,运算后的结果就是最后三位二进制做&运算后的值,最终,就是%运算后的余数,我想,这就是容量必须为2的幂的原因。HashTable中的实现对容量的大小没有规定,最终的bucketIndex是通过取余来运算的。注:我在考虑这样做是否真的是最好,规定容量大小一定为2的幂会浪费许多空间成本,而时间上的优化针对当今越来越牛逼的CPU是否还提升了许多,有些资料上说,CPU处理除法和取余运算是最慢的,这个应该取决于语言调用CPU指令的顺序和CPU底层实现的架构,也是有待验证,可以用Java写段程序测试一下,不过我想,CPU也是通过人来实现,人脑运算的速度应该是加法>减法>乘法>除法>取模(这里指的是正常的算法,不包括速算),CPU也应是如此。
       通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点,可以想想为什么)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子(实际上就是最大条目数小于初始容量*加载因子),则不会发生 rehash 操作。 
       如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 当HashMap存放的元素越来越多,到达临界值(阀值)threshold时,就要对Entry数组扩容,这是Java集合类框架最大的魅力,HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算bucketIndex,再存放到新数组中去,也就是所谓的rehash。HashMap默认初始容量16,加载因子0.75,也就是说最多能放16*0.75=12个元素,当put第13个时,HashMap将发生rehash,rehash的一系列处理比较影响性能,所以当我们需要向HashMap存放较多元素时,最好指定合适的初始容量和加载因子,否则HashMap默认只能存12个元素,将会发生多次rehash操作。
       HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败。注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。至于为什么通过迭代器自身的remove或add方法就不会出现这个问题,可以参考我之前的文章List比较好玩的操作中第三个和第四个示例。
       HashMap是线程不安全的实现,而HashTable是线程安全的实现,关于线程安全与不安全可以参考我之前的文章Java线程(一):线程安全与不安全,所谓线程不安全,就是在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题,多线程和单线程的区别:单线程只有一条执行路径,而多线程是并发执行(非并行),会有多条执行路径。如果HashMap是只读的(加载一次,以后只有读取,不会发生结构上的修改),那使用没有问题。那如果HashMap是可写的(会发生结构上的修改),则会引发诸多问题,如上面的fail-fast,也可以看下这里,这里就不去研究了。
        那在多线程下使用HashMap我们需要怎么做,几种方案:

  • 在外部包装HashMap,实现同步机制
  • 使用Map m = Collections.synchronizedMap(new HashMap(...));,这里就是对HashMap做了一次包装
  • 使用java.util.HashTable,效率最低
  • 使用java.util.concurrent.ConcurrentHashMap,相对安全,效率较高

 

分享到:
评论

相关推荐

    深入解析java HashMap实现原理

    主要介绍了深入解析java HashMap实现原理的相关资料,需要的朋友可以参考下

    HashMap新增数据原理.docx

    深入理解HASHMAP底层原理,通过对源码进行解析分析HASHMAP执行过程,一篇文章帮助你深刻理解HASHMAP

    Java面试题深入解析:在互联网公司面试程序员需要留意的六个问题.docx

    # Java面试题深入解析:在互联网公司面试程序员需要留意的六个问题 在互联网公司中,Java程序员是极为重要的角色,因此Java面试题也是非常重要的一环。无论是初级Java程序员还是高级Java程序员,都需要留意以下六个...

    linkedList

    深入解析hashMap底层原理,非常深入的讲解了HashMap和相关的数据的等信息

    Java equals 方法与hashcode 方法的深入解析

    面试时经常会问起字符串比较相关的问题,比如:字符串比较时用的什么方法,内部实现如何?hashcode的作用,以及重写equal方法,为什么要重写hashcode方法?以下就为大家解答,需要的朋友可以参考下

    Java面试通关宝典:深度解读核心知识点与实战技巧,全面提升面试表现力与技术实力

    本文将对Java常见面试题进行总结和解析,旨在为准备面试的Java开发者提供全面而深入的学习参考。以下是一些关键的Java面试题目类别及其概述: Java集合框架:这部分问题关注ArrayList、LinkedList、HashMap、HashSet...

    c语言难点分析整理,C语言

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    JAVA高并发高性能高可用高扩展架构视频教程

    JAVA企业级基础课题(HashMap那些事) 企业架构师必备技能(JAVA核心技术反射) JavaWeb之基础(手写实现Tomcat服务器) java多线程编程 纯手写实现SpringIOC实现过程 JEE企业级开发(企业级项目开发权威指南) 网络爬虫之...

    免费下载:C语言难点分析整理.doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    C语言难点分析整理.doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组...

    高级C语言 C 语言编程要点

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    高级进阶c语言教程..doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    史上最强的C语言资料

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    高级C语言详解

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    C语言难点分析整理

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    一个java正则表达式工具类源代码.zip(内含Regexp.java文件)

    这个工具类目前主要有25种正规表达式(有些不常用,但那时才仔细深入的研究了一下正规,写上瘾了,就当时能想到的都写了): 1.匹配图象; 2 匹配email地址; 3 匹配匹配并提取url ; 4 匹配并提取http ; 5.匹配日期 6...

    工程硕士学位论文 基于Android+HTML5的移动Web项目高效开发探究

    工程硕士学位论文 ...研究生姓名: 唐帅 导师姓名: 罗军舟 教授 苏生 教授 申请学位类别 工 程 硕 士 学位授予单位 东 南 大 学 工程领域名称 软 件 工 程 论文答辩日期 ...学位授予日期 答辩委员会主席 评阅人 ...

    高级C语言.PDF

    1. C 语言中的指针和内存泄漏 ............................................................................................................. 5 2. C语言难点分析整理 ..........................................

Global site tag (gtag.js) - Google Analytics