HashMap源码


HashMap的存储结构,如下图所示:


紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

1、首先看链表中节点的数据结构:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // Entry是单向链表。 (1.7以前)     
  2. // 它是 “HashMap链式存储法”对应的链表。      
  3. // 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数      
  4. static class Entry<K,V> implements Map.Entry<K,V> {      
  5.     final K key;      
  6.     V value;      
  7.     // 指向下一个节点      
  8.     Entry<K,V> next;      
  9.     final int hash;      
  10.     
  11.     // 构造函数。      
  12.     // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"      
  13.     Entry(int h, K k, V v, Entry<K,V> n) {      
  14.         value = v;      
  15.         next = n;      
  16.         key = k;      
  17.         hash = h;      
  18.     }      
  19.     
  20.     public final K getKey() {      
  21.         return key;      
  22.     }      
  23.     
  24.     public final V getValue() {      
  25.         return value;      
  26.     }      
  27.     
  28.     public final V setValue(V newValue) {      
  29.         V oldValue = value;      
  30.         value = newValue;      
  31.         return oldValue;      
  32.     }      
  33.     
  34.     // 判断两个Entry是否相等      
  35.     // 若两个Entry的“key”和“value”都相等,则返回true。      
  36.     // 否则,返回false      
  37.     public final boolean equals(Object o) {      
  38.         if (!(o instanceof Map.Entry))      
  39.             return false;      
  40.         Map.Entry e = (Map.Entry)o;      
  41.         Object k1 = getKey();      
  42.         Object k2 = e.getKey();      
  43.         if (k1 == k2 || (k1 != null && k1.equals(k2))) {      
  44.             Object v1 = getValue();      
  45.             Object v2 = e.getValue();      
  46.             if (v1 == v2 || (v1 != null && v1.equals(v2)))      
  47.                 return true;      
  48.         }      
  49.         return false;      
  50.     }      
  51.     
  52.     // 实现hashCode()      
  53.     public final int hashCode() {      
  54.         return (key==null   ? 0 : key.hashCode()) ^      
  55.                (value==null ? 0 : value.hashCode());      
  56.     }      
  57.     
  58.     public final String toString() {      
  59.         return getKey() + "=" + getValue();      
  60.     }      
  61.     
  62.     // 当向HashMap中添加元素时,绘调用recordAccess()。      
  63.     // 这里不做任何处理      
  64.     void recordAccess(HashMap<K,V> m) {      
  65.     }      
  66.     
  67.     // 当从HashMap中删除元素时,绘调用recordRemoval()。      
  68.     // 这里不做任何处理      
  69.     void recordRemoval(HashMap<K,V> m) {      
  70.     }      
  71. }      
2、 HashMap中用的最多的两个方法put和get。先从比较简单的get方法着手,源码如下:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 获取key对应的value      
  2. public V get(Object key) {      
  3.     if (key == null)      
  4.         return getForNullKey();      
  5.     // 获取key的hash值      
  6.     int hash = hash(key.hashCode());      
  7.     // 在“该hash值对应的链表”上查找“键值等于key”的元素      
  8.     for (Entry<K,V> e = table[indexFor(hash, table.length)];      
  9.          e != null;      
  10.          e = e.next) {      
  11.         Object k;      
  12. /判断key是否相同    
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))      
  14.             return e.value;      
  15.     }    
  16. 没找到则返回null    
  17.     return null;      
  18. }      
  19.     
  20. // 获取“key为null”的元素的值      
  21. // HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!      
  22. private V getForNullKey() {      
  23.     for (Entry<K,V> e = table[0]; e != null; e = e.next) {      
  24.         if (e.key == null)      
  25.             return e.value;      
  26.     }      
  27.     return null;      
  28. }   
    如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中的key都是null。所以HashMap中允许key为null,但只能有一个key为null。
    如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null。
3、 put方法代码如下:

  

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 将“key-value”添加到HashMap中      
  2.   public V put(K key, V value) {      
  3.       // 若“key为null”,则将该键值对添加到table[0]中。      
  4.       if (key == null)      
  5.           return putForNullKey(value);      
  6.       // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。      
  7.       int hash = hash(key.hashCode());      
  8.       int i = indexFor(hash, table.length);      
  9.       for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
  10.           Object k;      
  11.           // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!      
  12.           if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {      
  13.               V oldValue = e.value;      
  14.               e.value = value;      
  15.               e.recordAccess(this);      
  16.               return oldValue;      
  17.           }      
  18.       }      
  19.     
  20.       // 若“该key”对应的键值对不存在,则将“key-value”添加到table中      
  21.       modCount++;    
  22. //将key-value添加到table[i]处    
  23.       addEntry(hash, key, value, i);      
  24.       return null;      
  25.   }     
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <pre name="code" class="java">// putForNullKey()的作用是将“key为null”键值对添加到table[0]位置      
  2. private V putForNullKey(V value) {      
  3.     for (Entry<K,V> e = table[0]; e != null; e = e.next) {      
  4.         if (e.key == null) {      
  5.             V oldValue = e.value;      
  6.             e.value = value;      
  7.             e.recordAccess(this);      
  8.             return oldValue;      
  9.         }      
  10.     }      
  11.     // 如果没有存在key为null的键值对,则直接题阿见到table[0]处!      
  12.     modCount++;      
  13.     addEntry(0null, value, 0);      
  14.     return null;      
  15. }     

      如果key为null,则将其添加到table[0]对应的链表中 
  
     如果key不为null,则同样先求出key的hash值,根据hash值得出在table中的索引,而后遍历对应的单链表,如果单链表中存在与目标key相等的键值对,则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置(每次新插入的节点都是放在头结点的位置)

4、addEntry方法实现的,它的源码如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。      
  2. void addEntry(int hash, K key, V value, int bucketIndex) {      
  3.     // 保存“bucketIndex”位置的值到“e”中      
  4.     Entry<K,V> e = table[bucketIndex];      
  5.     // 设置“bucketIndex”位置的元素为“新Entry”,      
  6.     // 设置“e”为“新Entry的下一个节点”      
  7.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      
  8.     // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小      
  9.     if (size++ >= threshold)      
  10.         resize(2 * table.length);      
  11. }      
      注意这里倒数第三行的构造方法,将key-value键值对赋给table[bucketIndex],并将其next指向元素e,这便将key-value放到了头结点中,并将之前的头结点接在了它的后面。该方法也说明,每次put键值对的时候, 总是将新的该键值对放在table[bucketIndex]处(即头结点处)。
    两外注意最后两行代码,每次加入键值对时,都要判断当前已用的槽的数目是否大于等于阀值(容量*加载因子),如果大于等于,则进行扩容,将容量扩为原来容量的2倍。
5、扩容( resize)的方法的源码如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 重新调整HashMap的大小,newCapacity是调整后的单位      
  2. void resize(int newCapacity) {      
  3.     Entry[] oldTable = table;      
  4.     int oldCapacity = oldTable.length;      
  5.     if (oldCapacity == MAXIMUM_CAPACITY) {      
  6.         threshold = Integer.MAX_VALUE;      
  7.         return;      
  8.     }      
  9.     
  10.     // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,      
  11.     // 然后,将“新HashMap”赋值给“旧HashMap”。      
  12.     Entry[] newTable = new Entry[newCapacity];      
  13.     transfer(newTable);      
  14.     table = newTable;      
  15.     threshold = (int)(newCapacity * loadFactor);      
  16. }      
很明显,是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。transfer方法的源码如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 将HashMap中的全部元素都添加到newTable中      
  2. void transfer(Entry[] newTable) {      
  3.     Entry[] src = table;      
  4.     int newCapacity = newTable.length;      
  5.     for (int j = 0; j < src.length; j++) {      
  6.         Entry<K,V> e = src[j];      
  7.         if (e != null) {      
  8.             src[j] = null;      
  9.             do {      
  10.                 Entry<K,V> next = e.next;      
  11.                 int i = indexFor(e.hash, newCapacity);      
  12.                 e.next = newTable[i];      
  13.                 newTable[i] = e;      
  14.                 e = next;      
  15.             } while (e != null);      
  16.         }      
  17.     }      
  18. }      
很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。
6、求hash值和索引值的方法,这两个方法便是HashMap设计的最为核心的部分,二者结合能保证哈希表中的元素尽可能均匀地散列。计算哈希值的方法如下:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static int hash(int h) {    
  2.         h ^= (h >>> 20) ^ (h >>> 12);    
  3.         return h ^ (h >>> 7) ^ (h >>> 4);    
  4.     }    
它只是一个数学公式,IDK这样设计对hash值的计算,自然有它的好处,至于为什么这样设计,我们这里不去追究,只要明白一点,用的位的操作使hash值的计算效率很高。
    由hash值找到对应索引的方法如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static int indexFor(int h, int length) {    
  2.         return h & (length-1);    
  3.     }    
在 HashMap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 HashMap 的数据结构是数组和链表的结合,所以我们当然希望这个 HashMap 里面的 元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
   对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把 hash 值对数组长度length取模运算(HashTable中是这样做的),这样一来,元素的分布相对来说是比较均匀的。但是, “ 模 ” 运算的消耗还是比较大的,在 HashMap 中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。
      indexFor这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap 底层数组的长度总是 2 的 n 次方,这是HashMap 在速度上的优化。在 HashMap 构造器中有如下代码: 这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap 底层数组的长度总是 2 的 n 次方,这是HashMap 在速度上的优化。在 HashMap 构造器中有如下代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int  capacity =  1 ;    
  2.     while  (capacity < initialCapacity)    
  3.         capacity <<= 1 ;   
这段代码保证初始化时 HashMap 的容量总是 2 的 n 次方,即底层数组的长度总是为 2 的 n 次方。
当 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的整数次幂??????
        (1)首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;

        (2)其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。


总结: HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。 HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据int hash = hash(key.hashCode()); int i = indexFor(hash, table.length);hash 算法来决定其在数组中的存储位置,在根据equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个 Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该 Entry 。

 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map ,那么将抛出ConcurrentModificationException ,这就是所谓 fail-fast 策略。
   这一策略在源码中的实现是通过 modCount 域, modCount 顾名思义就是修改次数,对 HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值