JAVA hashmap详解

1.    HashMap概述:java

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供全部可选的映射操做,并容许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。算法

 

2.    HashMap的数据结构:编程

   java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),全部的数据结构均可以用这两个基本结构来构造的,HashMap也不例外。HashMap其实是一个“链表散列”的数据结构,即数组和链表的结合体。数组

   从上图中能够看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。安全

   源码以下:数据结构

 

Java代码 复制代码  收藏代码
  1. /**  
  2.  * The table, resized as necessary. Length MUST Always be a power of two.  
  3.  */  
  4. transient Entry[] table;   
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {   
  7.     final K key;   
  8.     V value;   
  9.     Entry<K,V> next;   
  10.     final int hash;   
  11.     ……   
  12. }  
[java] view plain copy
  1. /** 
  2.  * The table, resized as necessary. Length MUST Always be a power of two. 
  3.  */  
  4. transient Entry[] table;  
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {  
  7.     final K key;  
  8.     V value;  
  9.     Entry<K,V> next;  
  10.     final int hash;  
  11.     ……  
  12. }  

   能够看出,Entry就是数组中的元素,每一个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。并发

 

3.    HashMap的存取实现:编程语言

   1) 存储:性能

 

Java代码 复制代码  收藏代码
  1. public V put(K key, V value) {   
  2.     // HashMap容许存放null键和null值。   
  3.     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。   
  4.     if (key == null)   
  5.         return putForNullKey(value);   
  6.     // 根据key的keyCode从新计算hash值。   
  7.     int hash = hash(key.hashCode());   
  8.     // 搜索指定hash值在对应table中的索引。   
  9.     int i = indexFor(hash, table.length);   
  10.     // 若是 i 索引处的 Entry 不为 null,经过循环不断遍历 e 元素的下一个元素。   
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  12.         Object k;   
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  14.             V oldValue = e.value;   
  15.             e.value = value;   
  16.             e.recordAccess(this);   
  17.             return oldValue;   
  18.         }   
  19.     }   
  20.     // 若是i索引处的Entry为null,代表此处尚未Entry。   
  21.     modCount++;   
  22.     // 将key、value添加到i索引处。   
  23.     addEntry(hash, key, value, i);   
  24.     return null;   
  25. }  
[java] view plain copy
  1. public V put(K key, V value) {  
  2.     // HashMap容许存放null键和null值。  
  3.     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
  4.     if (key == null)  
  5.         return putForNullKey(value);  
  6.     // 根据key的keyCode从新计算hash值。  
  7.     int hash = hash(key.hashCode());  
  8.     // 搜索指定hash值在对应table中的索引。  
  9.     int i = indexFor(hash, table.length);  
  10.     // 若是 i 索引处的 Entry 不为 null,经过循环不断遍历 e 元素的下一个元素。  
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  12.         Object k;  
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             V oldValue = e.value;  
  15.             e.value = value;  
  16.             e.recordAccess(this);  
  17.             return oldValue;  
  18.         }  
  19.     }  
  20.     // 若是i索引处的Entry为null,代表此处尚未Entry。  
  21.     modCount++;  
  22.     // 将key、value添加到i索引处。  
  23.     addEntry(hash, key, value, i);  
  24.     return null;  
  25. }  

 

   从上面的源代码中能够看出:当咱们往HashMapput元素的时候,先根据keyhashCode从新计算hash值,根据hash值获得这个元素在数组中的位置(即下标),若是数组该位置上已经存放有其余元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。若是数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。 优化

   addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组tablei索引处。addEntry HashMap 提供的一个包访问权限的方法,代码以下:

 

Java代码 复制代码  收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {   
  2.     // 获取指定 bucketIndex 索引处的 Entry    
  3.     Entry<K,V> e = table[bucketIndex];   
  4.     // 将新建立的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry   
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   
  6.     // 若是 Map 中的 key-value 对的数量超过了极限   
  7.     if (size++ >= threshold)   
  8.     // 把 table 对象的长度扩充到原来的2倍。   
  9.         resize(2 * table.length);   
  10. }  
[java] view plain copy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     // 获取指定 bucketIndex 索引处的 Entry   
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     // 将新建立的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  6.     // 若是 Map 中的 key-value 对的数量超过了极限  
  7.     if (size++ >= threshold)  
  8.     // 把 table 对象的长度扩充到原来的2倍。  
  9.         resize(2 * table.length);  
  10. }  

 

   当系统决定存储HashMap中的key-value对时,彻底没有考虑Entry中的value,仅仅只是根据key来计算并决定每一个Entry的存储位置。咱们彻底能够把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置以后,value 随之保存在那里便可。

   hash(int h)方法根据keyhashCode从新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,形成的hash冲突。

 

Java代码 复制代码  收藏代码
  1. static int hash(int h) {   
  2.     h ^= (h >>> 20) ^ (h >>> 12);   
  3.     return h ^ (h >>> 7) ^ (h >>> 4);   
  4. }  
[java] view plain copy
  1. static int hash(int h) {  
  2.     h ^= (h >>> 20) ^ (h >>> 12);  
  3.     return h ^ (h >>> 7) ^ (h >>> 4);  
  4. }  

 

 

   咱们能够看到在HashMap中要找到某个元素,须要根据keyhash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,因此咱们固然但愿这个HashMap里面的元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,那么当咱们用hash算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

   对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算获得的 hash 码值老是相同的。咱们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来讲是比较均匀的。可是,运算的消耗仍是比较大的,在HashMap中是这样作的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪一个索引处。indexFor(int h, int length) 方法的代码以下:

 

Java代码 复制代码  收藏代码
  1. static int indexFor(int h, int length) {   
  2.     return h & (length-1);   
  3. }  
[java] view plain copy
  1. static int indexFor(int h, int length) {  
  2.     return h & (length-1);  
  3. }  

 

 

   这个方法很是巧妙,它经过 h & (table.length -1) 来获得该对象的保存位,而HashMap底层数组的长度老是 2 n 次方,这是HashMap在速度上的优化。在 HashMap 构造器中有以下代码:

Java代码 复制代码  收藏代码
  1. int capacity = 1;   
  2.     while (capacity < initialCapacity)   
  3.         capacity <<= 1;  
[java] view plain copy
  1. int capacity = 1;  
  2.     while (capacity < initialCapacity)  
  3.         capacity <<= 1;  

   这段代码保证初始化时HashMap的容量老是2n次方,即底层数组的长度老是为2n次方。

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

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

   假设数组长度分别为1516,优化后的hash码分别为89,那么&运算后的结果以下:

       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-11110的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,89会被放到数组中的同一个位置上造成链表,那么查询的时候就须要遍历这个链表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为15的时候,hash值会与15-11110)进行,那么最后一位永远是0,而0001001101011001101101111101这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!而当数组长度为16时,即为2n次方时,2n-1获得的二进制数的每一个位上的值都为1,这使得在低位上&时,获得的和原hash的低位相同,加之hash(int h)方法对keyhashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上造成链表。

   

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

   根据上面 put 方法的源代码能够看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key hashCode() 返回值决定该 Entry 的存储位置:若是两个 Entry key hashCode() 返回值相同,那它们的存储位置相同。若是这两个 Entry key 经过 equals 比较返回 true,新添加 Entry value 将覆盖集合中原有 Entry value,但key不会覆盖。若是这两个 Entry key 经过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 造成 Entry 链,并且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

 

 

   2) 读取:

 

Java代码 复制代码  收藏代码
  1. public V get(Object key) {   
  2.     if (key == null)   
  3.         return getForNullKey();   
  4.     int hash = hash(key.hashCode());   
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];   
  6.         e != null;   
  7.         e = e.next) {   
  8.         Object k;   
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))   
  10.             return e.value;   
  11.     }   
  12.     return null;   
  13. }  
[java] view plain copy
  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.         e != null;  
  7.         e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;  
  11.     }  
  12.     return null;  
  13. }  

 

 

   有了上面存储时的hash算法做为基础,理解起来这段代码就很容易了。从上面的源代码中能够看出:从HashMapget元素时,首先计算keyhashCode,找到数组中对应位置的某一元素,而后经过keyequals方法在对应位置的链表中找到须要的元素。

  

   3) 概括起来简单地说,HashMap 在底层将 key-value 当成一个总体进行处理,这个总体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存全部的 key-value 对,当须要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当须要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry

 

4.    HashMapresizerehash):

   HashMap中的元素愈来愈多的时候,hash冲突的概率也就愈来愈高,由于数组的长度是固定的。因此为了提升查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,这是一个经常使用的操做,而在HashMap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize

   那么HashMap何时进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认状况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知HashMap中元素的个数,那么预设元素的个数可以有效的提升HashMap的性能。

 

 

5.    HashMap的性能参数:

   HashMap 包含以下几个构造器:

   HashMap():构建一个初始容量为 16,负载因子为 0.75 HashMap

   HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 HashMap

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子建立一个 HashMap

   HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor

   initialCapacityHashMap的最大容量,即为底层数组的长度。

   loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来讲,查找一个元素的平均时间是O(1+a),所以若是负载因子越大,对空间的利用更充分,然然后果是查找效率的下降;若是负载因子过小,那么散列表的数据将过于稀疏,对空间形成严重浪费。

   HashMap的实现中,经过threshold字段来判断HashMap的最大容量:

Java代码 复制代码  收藏代码
  1. threshold = (int)(capacity * loadFactor);  
[java] view plain copy
  1. threshold = (int)(capacity * loadFactor);  

   结合负载因子的定义公式可知,threshold就是在此loadFactorcapacity对应下容许的最大元素数目,超过这个数目就从新resize,以下降实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

 

Java代码 复制代码  收藏代码
  1. if (size++ >= threshold)      
  2.     resize(2 * table.length);    
[java] view plain copy
  1. if (size++ >= threshold)     
  2.     resize(2 * table.length);    

 

6.    Fail-Fast机制:

   咱们知道java.util.HashMap不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

   这一策略在源码中的实现是经过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增长这个值,那么在迭代器初始化过程当中会将这个值赋给迭代器的expectedModCount

 

Java代码 复制代码  收藏代码
  1. HashIterator() {   
  2.     expectedModCount = modCount;   
  3.     if (size > 0) { // advance to first entry   
  4.     Entry[] t = table;   
  5.     while (index < t.length && (next = t[index++]) == null)   
  6.         ;   
  7.     }   
  8. }  
[java] view plain copy
  1. HashIterator() {  
  2.     expectedModCount = modCount;  
  3.     if (size > 0) { // advance to first entry  
  4.     Entry[] t = table;  
  5.     while (index < t.length && (next = t[index++]) == null)  
  6.         ;  
  7.     }  
  8. }  

 

 

   在迭代过程当中,判断modCountexpectedModCount是否相等,若是不相等就表示已经有其余线程修改了Map:

   注意到modCount声明为volatile,保证线程之间修改的可见性。

 

Java代码 复制代码  收藏代码
  1. final Entry<K,V> nextEntry() {      
  2.     if (modCount != expectedModCount)      
  3.         throw new ConcurrentModificationException();  
[java] view plain copy
  1. final Entry<K,V> nextEntry() {     
  2.     if (modCount != expectedModCount)     
  3.         throw new ConcurrentModificationException();  

 

   HashMapAPI中指出:

   由全部HashMap类的“collection 视图方法所返回的迭代器都是快速失败的:在迭代器建立以后,若是从结构上对映射进行修改,除非经过迭代器自己的 remove 方法,其余任什么时候间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。所以,面对并发的修改,迭代器很快就会彻底失败,而不冒在未来不肯定的时间发生任意不肯定行为的风险。

   注意,迭代器的快速失败行为不能获得保证,通常来讲,存在非同步的并发修改时,不可能做出任何坚定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。所以,编写依赖于此异常的程序的作法是错误的,正确作法是:迭代器的快速失败行为应该仅用于检测程序错误。

 

 

转载自http://blog.csdn.net/xiancaieeee/article/details/7768506

相关文章
相关标签/搜索