0x00 背景知识 
首先HashMap是基于hash表的,hash表的相关知识及其存储原理,可以参考我之前写的一篇文章:查找算法总结 ,要着重看此文章关于冲突处理所讲述的拉链法。
我们仅在这稍微明确一点概念:首先对于拉链式的hash表,其是由数组和链表共同组成的,当然Java中还引入了红黑树,当链表长度大于8时,将链表转为红黑树以提高查找效率。我们先来看上面的这个图,保存这个hash表的数组称之为桶(bucket),而每个桶中保存了一系列的数据入口(entries)。
同理,我们在这明确一个概念“等同 ”:我们假定如果两个对象其hashCode相同,且任一一个调用equals方法与另一个返回true,即认为两个对象等同。
同时,在HashMap中有如下几个量需要注意,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 transient  int  size;int  threshold;final  float  loadFactor;
 
HashMap和HashTable的区别
首先,HashMap是非线程安全的,而HashTable则是线程安全的,也因此,HashMap的效率较高。除此之外,在HashMap中null可以用来作为键,而HashTable则不可以,否则抛出NullPointerException。
HashMap和HashSet的区别
HashSet底层基于HashMap实现,其实现了Set接口,仅用来存储对象而不是键值对。而且HashSet较HashMap效率低。
HashMap的长度为什么是2的幂次方
因为经计算出来的hash值需要对数组长度进行取余运算,然后用余数定位其在hash表中的位置,为了优化取余运算的效率,人们发现取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作,即:hash%length等价于hash&(length-1),前提是length是2的n次方,采用二进制位操作&,相对于%能够提高运算效率。
0x01 put方法 
先来看一张流程图:
图片非原创,来自文章:HashMap原理深入理解 
 
正好我们就着源代码来说说这张图,首先我们来看put方法的源代码:
1 2 3 public  V put (K key, V value)  {    return  putVal(hash(key), key, value, false , true ); } 
 
其直接将工作转交至了putVal方法中,我们直接来看putVal的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 final  V putVal (int  hash, K key, V value, boolean  onlyIfAbsent,                 boolean  evict)  {         Node<K,V>[] tab; Node<K,V> p; int  n, i;     if  ((tab = table) == null  || (n = tab.length) == 0 )         n = (tab = resize()).length;                         if  ((p = tab[i = (n - 1 ) & hash]) == null )         tab[i] = newNode(hash, key, value, null );          else  {         Node<K,V> e; K k;                           if  (p.hash == hash &&             ((k = p.key) == key || (key != null  && key.equals(k))))                          e = p;                  else  if  (p instanceof  TreeNode)                          e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value);                   else  {             for  (int  binCount  =  0 ; ; ++binCount) {                                  if  ((e = p.next) == null ) {                                          p.next = newNode(hash, key, value, null );                                                                                    if  (binCount >= TREEIFY_THRESHOLD - 1 )                          treeifyBin(tab, hash);                     break ;                 }                                  if  (e.hash == hash &&                     ((k = e.key) == key || (key != null  && key.equals(k))))                     break ;                 p = e;             }         }                           if  (e != null ) {              V  oldValue  =  e.value;                                       if  (!onlyIfAbsent || oldValue == null )                 e.value = value;             afterNodeAccess(e);             return  oldValue;         }     }     ++modCount;           if  (++size > threshold)         resize();     afterNodeInsertion(evict);     return  null ; } 
 
在上述两个代码中,我们可以看到两个方法afterNodeAccess和afterNodeInsertion这两个方法在HashMap里面的实现是空的,在此处什么都不做,其注释中写道:Callbacks to allow LinkedHashMap  post-actions,即适用于LinkedHashMap 实现一些后置功能的回调函数。
JDK7 put方法为什么线程不安全 
以下内容仅适用于JDK7之前的版本,即对链表的插入方法为头插法,JDK8以后,链表的插入方法改为了尾插法,从此之后就不存在这个问题了 。
众所周知,HashMap不是线程安全的,多线程的情况下操作HashMap的put操作有可能导致死循环,原因在于HashMap扩容使用的resize方法,由于扩容是新建一个数组,然后复制原数据到数组,由于数组下标挂有链表,所以需要复制链表,但多线程操作有可能导致环形链表。
我们先来看JDK7 HashMap实现中的transfer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void  transfer (Entry[] newTable, boolean  rehash)  {    int  newCapacity  =  newTable.length;     for  (Entry<K,V> e : table) {         while (null  != e) {             Entry<K,V> next = e.next;             if  (rehash) {                 e.hash = null  == e.key ? 0  : hash(e.key);             }             int  i  =  indexFor(e.hash, newCapacity);             e.next = newTable[i];              newTable[i] = e;              e = next;          }     } } 
 
我们假设没有执行resize之前Entry数组的结构如下:
则其在单线程的情况下,正确执行扩容操作后的样子如下:
好了,现在我们来看多线程的情况,假设线程A执行扩容操作,在上述代码片中标记部分线程A被挂起,则目前线程A内的状态如下:
这个时候线程B介入了,线程B也想进行扩容操作,而且恰好线程B的扩容操作还操作完了,则线程B内的状态如下:
这是对于值为3, 7, 5的这三个对象来说,其状态为:
1 2 tab[1 ]=5 , 5. next=null  tab[3 ]=7 , 7. next=3 , 3. next=null  
 
然后CPU又切换时间片到线程A,线程A继续运行,在Java中代码中的临时变量e和next实际上都为引用类型变量。线程A继续操作,线程A的newTable变为了如下状态:
继续循环:
1 2 3 4 5 e=7  next=e.next ----> next=3 【从主存中取值】 e.next=newTable[3 ] ----> e.next=3 (7. next=3 )【从主存中取值】 newTable[3 ]=e ----> newTable[3 ]=7  e=next ----> e=3  
 
此时状态如下:
e=3于是又再一次进入循环:
1 2 3 4 5 e=3  next=e.next ----> next=null  e.next=newTable[3 ] ----> e.next=7 (3. next=7 ) newTable[3 ]=e ----> newTable[3 ]=3  e=next ----> e=null  
 
此时线程状态如下,并结束循环:
此时newTable[3]中所保存的链表即成了循环链表,当查找一个元素,假设说8,经计算indexOf(8) = 3,那么将会在table[3]所存储链表中查找,此时get(8)操作即为一个死循环。
JDK8之后的put方法为什么线程不安全? 
JDK8中链表的插入方法由JDK7中的头插入改为了尾插入,所以上面会导致死循环的那个问题在JDK8中已经不复存在了。但HashMap依旧是线程不安全的,因为写入丢失。
现在考虑一种情况,2个线程同时要向一个HashMap中插入元素,而这两个元素恰好又有相同的hash值,假设这个hash值所对应的数组下标所在位置原先是没有元素的,那线程1直接进行赋值,然后CPU切换到了线程2,线程2又进行了一次直接复制,线程2赋的值覆盖了线程1的值,导致了写入丢失的问题。
其次,如果线程1和2向HashMap插入元素时,需要进行扩容操作的话,线程1和线程2都会各自生成各自的newTab,然后最后tab = resize(),到最后table里面只保存了最后一个线程的newTab,其余线程的均会丢失。
0x02 get方法 
先来看一下get方法:
1 2 3 4 public  V get (Object key)  {    Node<K,V> e;     return  (e = getNode(hash(key), key)) == null  ? null  : e.value; } 
 
其主要工作在getNode方法中,用于通过键值hash和键值对象获取Node,如果其得到的值为null那就返回null,否则返回Node对象的value,我们着重来看getNode方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 final  Node<K,V> getNode (int  hash, Object key)  {    Node<K,V>[] tab; Node<K,V> first, e; int  n; K k;               if  ((tab = table) != null  && (n = tab.length) > 0  &&         (first = tab[(n - 1 ) & hash]) != null ) {                  if  (first.hash == hash &&              ((k = first.key) == key || (key != null  && key.equals(k))))             return  first;         if  ((e = first.next) != null ) {                          if  (first instanceof  TreeNode)                                  return  ((TreeNode<K,V>)first).getTreeNode(hash, key);                          do  {                 if  (e.hash == hash &&                     ((k = e.key) == key || (key != null  && key.equals(k))))                     return  e;             } while  ((e = e.next) != null );         }     }           return  null ; } 
 
0x03 remove方法 
remove方法用于从HashMap中移除一个元素,代码如下:
1 2 3 4 public  V remove (Object key)  {    Node<K,V> e;     return  (e = removeNode(hash(key), key, null , false , true )) == null  ? null  : e.value; } 
 
其主要工作在removeNode中,我们来看其源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 final  Node<K,V> removeNode (int  hash, Object key, Object value,                             boolean  matchValue, boolean  movable)  {    Node<K,V>[] tab; Node<K,V> p; int  n, index;          if  ((tab = table) != null  && (n = tab.length) > 0  &&                  (p = tab[index = (n - 1 ) & hash]) != null ) {         Node<K,V> node = null , e; K k; V v;.                           if  (p.hash == hash &&             ((k = p.key) == key || (key != null  && key.equals(k))))             node = p;         else  if  ((e = p.next) != null ) {                          if  (p instanceof  TreeNode)                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);             else  {                                  do  {                     if  (e.hash == hash &&                         ((k = e.key) == key ||                             (key != null  && key.equals(k)))) {                         node = e;                         break ;                     }                     p = e;                 } while  ((e = e.next) != null );             }         }         if  (node != null  && (!matchValue || (v = node.value) == value ||                                 (value != null  && value.equals(v)))) {                          if  (node instanceof  TreeNode)                                  ((TreeNode<K,V>)node).removeTreeNode(this , tab, movable);                          else  if  (node == p)                 tab[index] = node.next;             else                  p.next = node.next;             ++modCount;             --size;             afterNodeRemoval(node);             return  node;         }     }          return  null ; } 
 
0x04 resize方法 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 final  Node<K,V>[] resize() {    Node<K,V>[] oldTab = table;     int  oldCap  =  (oldTab == null ) ? 0  : oldTab.length;     int  oldThr  =  threshold;     int  newCap, newThr = 0 ;     if  (oldCap > 0 ) {         if  (oldCap >= MAXIMUM_CAPACITY) {                                                    threshold = Integer.MAX_VALUE;             return  oldTab;          }                  else  if  ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY &&                     oldCap >= DEFAULT_INITIAL_CAPACITY)                                       newThr = oldThr << 1 ;      }          else  if  (oldThr > 0 )          newCap = oldThr;          else  {           newCap = DEFAULT_INITIAL_CAPACITY;         newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);     }          if  (newThr == 0 ) {         float  ft  =  (float )newCap * loadFactor;                           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ?                     (int )ft : Integer.MAX_VALUE);     }     threshold = newThr;     @SuppressWarnings({"rawtypes","unchecked"})      Node<K,V>[] newTab = (Node<K,V>[])new  Node [newCap];     table = newTab;          if  (oldTab != null ) {         for  (int  j  =  0 ; j < oldCap; ++j) {             Node<K,V> e;             if  ((e = oldTab[j]) != null ) {                 oldTab[j] = null ;                 if  (e.next == null )                                          newTab[e.hash & (newCap - 1 )] = e;                 else  if  (e instanceof  TreeNode)                     ((TreeNode<K,V>)e).split(this , newTab, j, oldCap);                 else  {                                                                Node<K,V> loHead = null , loTail = null ;                                                               Node<K,V> hiHead = null , hiTail = null ;                     Node<K,V> next;                     do  {                         next = e.next;                                                  if  ((e.hash & oldCap) == 0 ) {                                                                                       if  (loTail == null )                                 loHead = e;                             else                                  loTail.next = e;                             loTail = e;                         }                         else  {                                                          if  (hiTail == null )                                 hiHead = e;                             else                                  hiTail.next = e;                             hiTail = e;                         }                     } while  ((e = next) != null );                     if  (loTail != null ) {                                                  loTail.next = null ;                                                                                                                             newTab[j] = loHead;                     }                     if  (hiTail != null ) {                                                  hiTail.next = null ;                         newTab[j + oldCap] = hiHead;                     }                 }             }         }     }     return  newTab; } 
 
对判断条件(e.hash & oldCap) == 0的理解 
扩容后重新计算元素在新表中的位置的时候,用到了一个非常巧妙的算法,首先我们每次重新计算,都是将原数组大小n乘以2。我们现在假设原数组大小为16,则新数组大小即为32,我们计算某hash值在数组中的位置时,使用的方法为e.hash & (n - 1),当n翻一番的时候,这个产生的结果只可能出现2种情况,一种是不变,如下:
1 2 3 4 5 6 7 8 扩容前n为16时: n-1          0000 0000 0000 0000 0000 0000 0000 1111 hash         1111 1111 1111 1111 0000 1111 0000 0101 hash & (n-1) 0000 0000 0000 0000 0000 0000 0000 0101 扩容后n为32时: n-1          0000 0000 0000 0000 0000 0000 0001 1111 hash         1111 1111 1111 1111 0000 1111 0000 0101 hash & (n-1) 0000 0000 0000 0000 0000 0000 0000 0101 
 
首先一个对象的hash值只与这个对象有关,与其他因素无关,所以扩容前后hash值是不变的,变的只有n-1的值,在上例中,扩容前后的这个元素在数组中的位置依旧保持不变
另一种是结果也跟着移动2次幂的位置,如下:
1 2 3 4 5 6 7 8 扩容前n为16时: n-1          0000 0000 0000 0000 0000 0000 0000 1111 hash         1111 1111 1111 1111 0000 1111 0001 0101 hash & (n-1) 0000 0000 0000 0000 0000 0000 0000 0101 扩容后n为32时: n-1          0000 0000 0000 0000 0000 0000 0001 1111 hash         1111 1111 1111 1111 0000 1111 0001 0101 hash & (n-1) 0000 0000 0000 0000 0000 0000 0001 0101 
 
在这种情况下,我们只需要看新增的那个bit位是0还是1就好了,是0的话索引没变,是1的话索引变成原索引加oldCap。
我们发现新增的那个bit位的位置实际上就是oldCap(n)的位置,如下:
1 2 3 4 5 n            0000 0000 0000 0000 0000 0000 0001 0000 扩容后数组位置不变的情况: hash         1111 1111 1111 1111 0000 1111 0000 0101 扩容后数组位置发生变化的情况: hash         1111 1111 1111 1111 0000 1111 0001 0101 
 
我们发现只要判断hash & n是否等于1就可以判断出扩容之后的位置是否发生变化来,由此判断条件(e.hash & oldCap) == 0的意思即为判断扩容后的位置是否发生变化。
0x05 hash方法 
下面再来看一下其采用的Hash函数:
1 2 3 4 static  final  int  hash (Object key)  {    int  h;     return  (key == null ) ? 0  : (h = key.hashCode()) ^ (h >>> 16 ); } 
 
显然,通过其hash函数所得的hash码值与这个对象的hashCode有着直接的关联,这就产生了一个问题,首先hashCode的返回值是依据当前对象在JVM中的内存地址算出来的,当我们用一个自定义类的对象来做key的话,同样内容的两个对象,get操作就会出现问题,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public  class  Main  {    public  static  void  main (String[] args)  {         Person  boy  =  new  Person ("Qun" , 22 );         Person  girl  =  new  Person ("Hui" , 23 );         HashMap<Person, Person> couples = new  HashMap <Person, Person>();         couples.put(boy, girl);         Person  someone  =  new  Person ("Qun" , 22 );         System.out.println(couples.get(someone));     } } public  class  Person  {    private  String name;     private  int  age;     public  Person (String name, int  age)  {         this .name = name;         this .age = age;     }           } 
 
get函数会返回null,someone和boy虽说看上去内容是一样的,但是实际上是两个对象,二者的hashCode会返回不一样的值,所得到的用于查找的hash码值也会不一样,所以get失败。好,我们来重写一下Person类的hashCode让他由属性name和age来决定,代码如下:
1 2 3 4 @Override public  int  hashCode ()  {    return  name.hashCode() + age; } 
 
TL;DR
String类型的对象所产生的hashCode值是由其所保存的字符串决定的,字符串相同,hashCode值也相同,字符串不同,hashCode值也不同。我们可以在其文档中,找到相关hashCode值产生算法的描述:
1 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 
 
 
这样就可以保证两个内容相同的Person对象产生的hashCode也相同了,让我们重新运行代码,依旧输出null,经检查发现get函数不仅会判断key的hash值相同,同时也会使用equals函数来判断两个对象是否等同,重写equals函数如下:
1 2 3 4 5 6 @Override public  boolean  equals (Object obj)  {    return  obj instanceof  Person &&               this .name.equals(((Person) obj).name) &&                this .age == ((Person) obj).age; } 
 
这样程序就返回了正确的结果。由此可以得到一个结论,查找HashMap时,get操作会先通过hashCode计算其相关的hash码值,当这个hash码值相同时,会调用equals方法来检验当前对象与数组中hash码值相同的对象是否等同,如果等同即会返回其关联的value。
这样的hash计算方法产生的hash碰撞的概率在其源码里有详细的介绍,摘抄如下:
Ideally, under random hashCodes, the frequency of 
nodes in bins follows a Poisson distribution 
(http://en.wikipedia.org/wiki/Poisson_distribution ) with a 
parameter of about 0.5 on average for the default resizing 
threshold of 0.75, although with a large variance because of 
resizing granularity. Ignoring variance, the expected 
occurrences of list size k are (exp(-0.5) pow(0.5, k) / 
factorial(k)). The first values are: 
0:    0.60653066 
1:    0.30326533 
2:    0.07581633 
3:    0.01263606 
4:    0.00157952 
5:    0.00015795 
6:    0.00001316 
7:    0.00000094 
8:    0.00000006 
more: less than 1 in ten million
 
大意是在理想状况下,链表内元素的个数是服从泊松分布(Poisson distribution)的,其还给出了链表内元素个数所出现的概率,例如链表内有1个元素的概率是0.3032,超过8个元素的概率就在千万分之一了。也就说真正需要转化成红黑树的概率其实是很小的。