HashMap的原理及实现

一.JDK 1.7

1.基本属性

  
  
继承关系

  
  
public class HashMap<K,V>extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

  
  
基本属性

  
  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16 

  
  
static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75

  
  
static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默认数组

  
  
transient int size;     //HashMap中元素的数量

  
  
int threshold;          //判断是否需要调整HashMap的容量


2.Entry是HashMap中的一个静态内部类。代码如下
 
 
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

3.数据存储结构


4.   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. }  

   从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

   addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组table的i索引处。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. }  

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

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

Java代码   收藏代码
  1. static int hash(int h) {  
  2.     h ^= (h >>> 20) ^ (h >>> 12);  
  3.     return h ^ (h >>> 7) ^ (h >>> 4);  
  4. }  

 

   我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是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. }  

 

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

Java代码   收藏代码
  1. int capacity = 1;  
  2.     while (capacity < initialCapacity)  
  3.         capacity <<= 1;  

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

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

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

   假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

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

   

   所以说,当数组长度为2的n次幂的时候,不同的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. }  

 

   有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

  

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

 

5.    HashMap的resize(rehash):

   当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

6.    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。

   initialCapacity:HashMap的最大容量,即为底层数组的长度。

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

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

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

Java代码   收藏代码
  1. threshold = (int)(capacity * loadFactor);  

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

 

Java代码   收藏代码
  1. if (size++ >= threshold)     
  2.     resize(2 * table.length); 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. }  

       从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

       addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组table的i索引处。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. }  

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

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

    Java代码   收藏代码
    1. static int hash(int h) {  
    2.     h ^= (h >>> 20) ^ (h >>> 12);  
    3.     return h ^ (h >>> 7) ^ (h >>> 4);  
    4. }  

     

       我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是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. }  

     

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

    Java代码   收藏代码
    1. int capacity = 1;  
    2.     while (capacity < initialCapacity)  
    3.         capacity <<= 1;  

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

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

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

       假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

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

       

       所以说,当数组长度为2的n次幂的时候,不同的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. }  

     

       有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

      

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

     

    二,JDK1.8中的涉及到的数据结构

    1,位桶数组

    [java]  view plain  copy
    1. transient Node<k,v>[] table;//存储(位桶)的数组</k,v>  
    2,数组元素Node<K,V>实现了Entry接口

    [java]  view plain  copy
    1. //Node是单向链表,它实现了Map.Entry接口  
    2. static class Node<k,v> implements Map.Entry<k,v> {  
    3.     final int hash;  
    4.     final K key;  
    5.     V value;  
    6.     Node<k,v> next;  
    7.     //构造函数Hash值 键 值 下一个节点  
    8.     Node(int hash, K key, V value, Node<k,v> next) {  
    9.         this.hash = hash;  
    10.         this.key = key;  
    11.         this.value = value;  
    12.         this.next = next;  
    13.     }  
    14.    
    15.     public final K getKey()        { return key; }  
    16.     public final V getValue()      { return value; }  
    17.     public final String toString() { return key + = + value; }  
    18.    
    19.     public final int hashCode() {  
    20.         return Objects.hashCode(key) ^ Objects.hashCode(value);  
    21.     }  
    22.    
    23.     public final V setValue(V newValue) {  
    24.         V oldValue = value;  
    25.         value = newValue;  
    26.         return oldValue;  
    27.     }  
    28.     //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true  
    29.     public final boolean equals(Object o) {  
    30.         if (o == this)  
    31.             return true;  
    32.         if (o instanceof Map.Entry) {  
    33.             Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o;  
    34.             if (Objects.equals(key, e.getKey()) &&  
    35.                 Objects.equals(value, e.getValue()))  
    36.                 return true;  
    37.         }  
    38.         return false;  
    39.     }  
    3,红黑树

    [java]  view plain  copy
    1. //红黑树  
    2. static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {  
    3.     TreeNode<k,v> parent;  // 父节点  
    4.     TreeNode<k,v> left; //左子树  
    5.     TreeNode<k,v> right;//右子树  
    6.     TreeNode<k,v> prev;    // needed to unlink next upon deletion  
    7.     boolean red;    //颜色属性  
    8.     TreeNode(int hash, K key, V val, Node<k,v> next) {  
    9.         super(hash, key, val, next);  
    10.     }  
    11.    
    12.     //返回当前节点的根节点  
    13.     final TreeNode<k,v> root() {  
    14.         for (TreeNode<k,v> r = this, p;;) {  
    15.             if ((p = r.parent) == null)  
    16.                 return r;  
    17.             r = p;  
    18.         }  
    19.     }  

    三,源码中的数据域

    加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率

    HashMap本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。

    [java]  view plain  copy
    1. public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable {  
    2.     private static final long serialVersionUID = 362498820763181265L;  
    3.     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4// aka 16  
    4.     static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量  
    5.     static final float DEFAULT_LOAD_FACTOR = 0.75f;//填充比  
    6.     //当add一个元素到某个位桶,其链表长度达到8时将链表转换为红黑树  
    7.     static final int TREEIFY_THRESHOLD = 8;  
    8.     static final int UNTREEIFY_THRESHOLD = 6;  
    9.     static final int MIN_TREEIFY_CAPACITY = 64;  
    10.     transient Node<k,v>[] table;//存储元素的数组  
    11.     transient Set<map.entry<k,v>> entrySet;  
    12.     transient int size;//存放元素的个数  
    13.     transient int modCount;//被修改的次数fast-fail机制  
    14.     int threshold;//临界值 当实际大小(容量*填充比)超过临界值时,会进行扩容   
    15.     final float loadFactor;//填充比(......后面略)  
    四,HashMap的构造函数

    HashMap的构造方法有4种,主要涉及到的参数有,指定初始容量,指定填充比和用来初始化的Map

    [java]  view plain  copy
    1. //构造函数1  
    2. public HashMap(int initialCapacity, float loadFactor) {  
    3.     //指定的初始容量非负  
    4.     if (initialCapacity < 0)  
    5.         throw new IllegalArgumentException(Illegal initial capacity:  +  
    6.                                            initialCapacity);  
    7.     //如果指定的初始容量大于最大容量,置为最大容量  
    8.     if (initialCapacity > MAXIMUM_CAPACITY)  
    9.         initialCapacity = MAXIMUM_CAPACITY;  
    10.     //填充比为正  
    11.     if (loadFactor <= 0 || Float.isNaN(loadFactor))  
    12.         throw new IllegalArgumentException(Illegal load factor:  +  
    13.                                            loadFactor);  
    14.     this.loadFactor = loadFactor;  
    15.     this.threshold = tableSizeFor(initialCapacity);//新的扩容临界值  
    16. }  
    17.    
    18. //构造函数2  
    19. public HashMap(int initialCapacity) {  
    20.     this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    21. }  
    22.    
    23. //构造函数3  
    24. public HashMap() {  
    25.     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted  
    26. }  
    27.    
    28. //构造函数4用m的元素初始化散列映射  
    29. public HashMap(Map<!--? extends K, ? extends V--> m) {  
    30.     this.loadFactor = DEFAULT_LOAD_FACTOR;  
    31.     putMapEntries(m, false);  
    32. }  
    五,HashMap的存取机制

    1,HashMap如何getValue值,看源码

    [java]  view plain  copy
    1. public V get(Object key) {  
    2.         Node<K,V> e;  
    3.         return (e = getNode(hash(key), key)) == null ? null : e.value;  
    4.     }  
    5.       /** 
    6.      * Implements Map.get and related methods 
    7.      * 
    8.      * @param hash hash for key 
    9.      * @param key the key 
    10.      * @return the node, or null if none 
    11.      */  
    12.     final Node<K,V> getNode(int hash, Object key) {  
    13.         Node<K,V>[] tab;//Entry对象数组  
    14.     Node<K,V> first,e; //在tab数组中经过散列的第一个位置  
    15.     int n;  
    16.     K k;  
    17.     /*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/  
    18.     //也就是说在一条链上的hash值相同的  
    19.         if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {  
    20.     /*检查第一个Node是不是要找的Node*/  
    21.             if (first.hash == hash && // always check first node  
    22.                 ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同  
    23.                 return first;  
    24.       /*检查first后面的node*/  
    25.             if ((e = first.next) != null) {  
    26.                 if (first instanceof TreeNode)  
    27.                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);  
    28.                 /*遍历后面的链表,找到key值和hash值都相同的Node*/  
    29.                 do {  
    30.                     if (e.hash == hash &&  
    31.                         ((k = e.key) == key || (key != null && key.equals(k))))  
    32.                         return e;  
    33.                 } while ((e = e.next) != null);  
    34.             }  
    35.         }  
    36.         return null;  
    37.     }  
    get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可

    2,HashMap如何put(key,value);看源码

    [java]  view plain  copy
    1. public V put(K key, V value) {  
    2.         return putVal(hash(key), key, value, falsetrue);  
    3.     }  
    4.      /** 
    5.      * Implements Map.put and related methods 
    6.      * 
    7.      * @param hash hash for key 
    8.      * @param key the key 
    9.      * @param value the value to put 
    10.      * @param onlyIfAbsent if true, don't change existing value 
    11.      * @param evict if false, the table is in creation mode. 
    12.      * @return previous value, or null if none 
    13.      */  
    14. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  
    15.                    boolean evict) {  
    16.         Node<K,V>[] tab;   
    17.     Node<K,V> p;   
    18.     int n, i;  
    19.         if ((tab = table) == null || (n = tab.length) == 0)  
    20.             n = (tab = resize()).length;  
    21.     /*如果table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/  
    22.         if ((p = tab[i = (n - 1) & hash]) == null)  
    23.             tab[i] = newNode(hash, key, value, null);  
    24.     /*表示有冲突,开始处理冲突*/  
    25.         else {  
    26.             Node<K,V> e;   
    27.         K k;  
    28.     /*检查第一个Node,p是不是要找的值*/  
    29.             if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))  
    30.                 e = p;  
    31.             else if (p instanceof TreeNode)  
    32.                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  
    33.             else {  
    34.                 for (int binCount = 0; ; ++binCount) {  
    35.         /*指针为空就挂在后面*/  
    36.                     if ((e = p.next) == null) {  
    37.                         p.next = newNode(hash, key, value, null);  
    38.                //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,               
    39.             //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行  
    40.                         //resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树  
    41.                         if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st  
    42.                             treeifyBin(tab, hash);  
    43.                         break;  
    44.                     }  
    45.         /*如果有相同的key值就结束遍历*/  
    46.                     if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))  
    47.                         break;  
    48.                     p = e;  
    49.                 }  
    50.             }  
    51.     /*就是链表上有相同的key值*/  
    52.             if (e != null) { // existing mapping for key,就是key的Value存在  
    53.                 V oldValue = e.value;  
    54.                 if (!onlyIfAbsent || oldValue == null)  
    55.                     e.value = value;  
    56.                 afterNodeAccess(e);  
    57.                 return oldValue;//返回存在的Value值  
    58.             }  
    59.         }  
    60.         ++modCount;  
    61.      /*如果当前大小大于门限,门限原本是初始容量*0.75*/  
    62.         if (++size > threshold)  
    63.             resize();//扩容两倍  
    64.         afterNodeInsertion(evict);  
    65.         return null;  
    66.     }  
    下面简单说下添加键值对put(key,value)的过程:
    1,判断键值对数组tab[]是否为空或为null,否则以默认大小resize();
    2,根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
    3,判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理

    六,HasMap的扩容机制resize();

    构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时

    [java]  view plain  copy
    1.  /** 
    2.     * Initializes or doubles table size.  If null, allocates in 
    3.     * accord with initial capacity target held in field threshold. 
    4.     * Otherwise, because we are using power-of-two expansion, the 
    5.     * elements from each bin must either stay at same index, or move 
    6.     * with a power of two offset in the new table. 
    7.     * 
    8.     * @return the table 
    9.     */  
    10.    final Node<K,V>[] resize() {  
    11.        Node<K,V>[] oldTab = table;  
    12.        int oldCap = (oldTab == null) ? 0 : oldTab.length;  
    13.        int oldThr = threshold;  
    14.        int newCap, newThr = 0;  
    15.       
    16. /*如果旧表的长度不是空*/  
    17.        if (oldCap > 0) {  
    18.            if (oldCap >= MAXIMUM_CAPACITY) {  
    19.                threshold = Integer.MAX_VALUE;  
    20.                return oldTab;  
    21.            }  
    22. /*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/  
    23.            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
    24.                     oldCap >= DEFAULT_INITIAL_CAPACITY)  
    25.       /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/  
    26.                newThr = oldThr << 1// double threshold  
    27.        }  
    28.     /*如果旧表的长度的是0,就是说第一次初始化表*/  
    29.        else if (oldThr > 0// initial capacity was placed in threshold  
    30.            newCap = oldThr;  
    31.        else {               // zero initial threshold signifies using defaults  
    32.            newCap = DEFAULT_INITIAL_CAPACITY;  
    33.            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
    34.        }  
    35.       
    36.       
    37.       
    38.        if (newThr == 0) {  
    39.            float ft = (float)newCap * loadFactor;//新表长度乘以加载因子  
    40.            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  
    41.                      (int)ft : Integer.MAX_VALUE);  
    42.        }  
    43.        threshold = newThr;  
    44.        @SuppressWarnings({"rawtypes","unchecked"})  
    45. /*下面开始构造新表,初始化表中的数据*/  
    46.        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  
    47.        table = newTab;//把新表赋值给table  
    48.        if (oldTab != null) {//原表不是空要把原表中数据移动到新表中      
    49.            /*遍历原来的旧表*/        
    50.            for (int j = 0; j < oldCap; ++j) {  
    51.                Node<K,V> e;  
    52.                if ((e = oldTab[j]) != null) {  
    53.                    oldTab[j] = null;  
    54.                    if (e.next == null)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置  
    55.                        newTab[e.hash & (newCap - 1)] = e;  
    56.                    else if (e instanceof TreeNode)  
    57.                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
    58. /*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重*/  
    59.                    else { // preserve order保证顺序  
    60.                 新计算在新表的位置,并进行搬运  
    61.                        Node<K,V> loHead = null, loTail = null;  
    62.                        Node<K,V> hiHead = null, hiTail = null;  
    63.                        Node<K,V> next;  
    64.                       
    65.                        do {  
    66.                            next = e.next;//记录下一个结点  
    67.           //新表是旧表的两倍容量,实例上就把单链表拆分为两队,  
    68.              //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对  
    69.                            if ((e.hash & oldCap) == 0) {  
    70.                                if (loTail == null)  
    71.                                    loHead = e;  
    72.                                else  
    73.                                    loTail.next = e;  
    74.                                loTail = e;  
    75.                            }  
    76.                            else {  
    77.                                if (hiTail == null)  
    78.                                    hiHead = e;  
    79.                                else  
    80.                                    hiTail.next = e;  
    81.                                hiTail = e;  
    82.                            }  
    83.                        } while ((e = next) != null);  
    84.                       
    85.                        if (loTail != null) {//lo队不为null,放在新表原位置  
    86.                            loTail.next = null;  
    87.                            newTab[j] = loHead;  
    88.                        }  
    89.                        if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置  
    90.                            hiTail.next = null;  
    91.                            newTab[j + oldCap] = hiHead;  
    92.                        }  
    93.                    }  
    94.                }  
    95.            }  
    96.        }  
    97.        return newTab;  
    98.    }  

    七,JDK1.8使用红黑树的改进

    在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
    在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)


    问题分析:

    你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。


    随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。

    JDK1.8HashMap的红黑树是这样解决的

             如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。

            它是如何工作的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值