简介 HashMap是Map经常使用的一个实现类,也是我们经常用到的一个集合,所以很有必要去了解一下。JDK8相对JDK7优化了不少,结构也改变了,所以这里就分开来学习了。
因为我英语不是特别好,所以有些源码的文档注释是机翻的,所以有些文档注释读起来怪怪的,只能理解一下大概意思。
JDK7 参数和变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 static  final  int  DEFAULT_INITIAL_CAPACITY  =  1  << 4 ; static  final  int  MAXIMUM_CAPACITY  =  1  << 30 ;static  final  float  DEFAULT_LOAD_FACTOR  =  0.75f ;
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    static  final  Entry<?,?>[] EMPTY_TABLE = {};    transient  Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;    transient  int  size;    int  threshold;    final  float  loadFactor;    transient  int  modCount;    static  final  int  ALTERNATIVE_HASHING_THRESHOLD_DEFAULT  =  Integer.MAX_VALUE;  transient  int  hashSeed  =  0 ;  private  transient  Set<Map.Entry<K,V>> entrySet = null ;    private  static  final  long  serialVersionUID  =  362498820763181265L ; 
构造器 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 public  HashMap ()  {    this (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public  HashMap (int  initialCapacity)  {    this (initialCapacity, DEFAULT_LOAD_FACTOR); } public  HashMap (int  initialCapacity, float  loadFactor)  {    if  (initialCapacity < 0 )         throw  new  IllegalArgumentException ("Illegal initial capacity: "  +                                            initialCapacity);     if  (initialCapacity > MAXIMUM_CAPACITY)         initialCapacity = MAXIMUM_CAPACITY;     if  (loadFactor <= 0  || Float.isNaN(loadFactor))         throw  new  IllegalArgumentException ("Illegal load factor: "  +                                            loadFactor);     this .loadFactor = loadFactor;     threshold = initialCapacity;     init(); } public  HashMap (Map<? extends K, ? extends V> m)  {    this (Math.max((int ) (m.size() / DEFAULT_LOAD_FACTOR) + 1 ,                   DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);     inflateTable(threshold);     putAllForCreate(m); } 
从构造方法来看,HashMap(int initialCapacity, float loadFactor)这个方法才是主体。接下来就来分析一下这个构造方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public  HashMap (int  initialCapacity, float  loadFactor)  {         if  (initialCapacity < 0 )         throw  new  IllegalArgumentException ("Illegal initial capacity: "  +                                            initialCapacity);          if  (initialCapacity > MAXIMUM_CAPACITY)         initialCapacity = MAXIMUM_CAPACITY;          if  (loadFactor <= 0  || Float.isNaN(loadFactor))         throw  new  IllegalArgumentException ("Illegal load factor: "  +                                            loadFactor);     this .loadFactor = loadFactor;     threshold = initialCapacity;          init(); } 
数据结构 构造方法看完了,现在来看看HashMap的数据结构。JDK7的HashMap是以数组为主,链表为辅的结构。大概结构如下。
每个链表被称为哈希表的桶,每个节点都存储了一对键值对。
这里就看出,每个节点就是哈希表的最小组成单位了,现在就来看一下这个节点类。JDK7时的节点类叫Entry。
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 static  class  Entry <K,V> implements  Map .Entry<K,V> {    final  K key;     V value;     Entry<K,V> next;     int  hash;          Entry(int  h, K k, V v, Entry<K,V> n) {         value = v;         next = n;         key = k;         hash = h;     }          public  final  K getKey ()  {         return  key;     }     public  final  V getValue ()  {         return  value;     }     public  final  V setValue (V newValue)  {         V  oldValue  =  value;         value = newValue;         return  oldValue;     }          public  final  boolean  equals (Object o)  {         if  (!(o instanceof  Map.Entry))             return  false ;         Map.Entry  e  =  (Map.Entry)o;         Object  k1  =  getKey();         Object  k2  =  e.getKey();         if  (k1 == k2 || (k1 != null  && k1.equals(k2))) {             Object  v1  =  getValue();             Object  v2  =  e.getValue();             if  (v1 == v2 || (v1 != null  && v1.equals(v2)))                 return  true ;         }         return  false ;     }          public  final  int  hashCode ()  {         return  Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());     }          public  final  String toString ()  {         return  getKey() + "="  + getValue();     }          void  recordAccess (HashMap<K,V> m)  {     }          void  recordRemoval (HashMap<K,V> m)  {     } } 
由此可以看出每个Entry都存储了四个值,一对键值对,一个hash,和一个next值。next指向链表的下一个节点,而hash则用来判断该节点放在数组哪一个位置。
put 创建了哈希表之后,就可以用put方法来存放键值对,现在就来看看put方法的源码。
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 public  V put (K key, V value)  {    if  (table == EMPTY_TABLE) {         inflateTable(threshold);     }     if  (key == null )         return  putForNullKey(value);     int  hash  =  hash(key);     int  i  =  indexFor(hash, table.length);     for  (Entry<K,V> e = table[i]; e != null ; e = e.next) {         Object k;         if  (e.hash == hash && ((k = e.key) == key || key.equals(k))) {             V  oldValue  =  e.value;             e.value = value;             e.recordAccess(this );             return  oldValue;         }     }     modCount++;     addEntry(hash, key, value, i);     return  null ; } 
put方法有很多门道,现在我们就来一步步解析。
第一步 1 2 3 if  (table == EMPTY_TABLE) {    inflateTable(threshold); } 
首先就是判断这个哈希表是不是一个空表。如果是就会调用inflateTable(threshold);这个方法,不是就进行下一步。我们去看看这个方法。
1 2 3 4 5 6 7 8 9 10 private  void  inflateTable (int  toSize)  {         int  capacity  =  roundUpToPowerOf2(toSize);          threshold = (int ) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1 );          table = new  Entry [capacity];          initHashSeedAsNeeded(capacity); } 
这个方法就是用来创建数组的,通过roundUpToPowerOf2方法来确定最小的能大于需要长度toSize的二次幂,是有点绕,那现在去看看这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private  static  int  roundUpToPowerOf2 (int  number)  {              return  number >= MAXIMUM_CAPACITY             ? MAXIMUM_CAPACITY             : (number > 1 ) ? Integer.highestOneBit((number - 1 ) << 1 ) : 1 ; } public  static  int  highestOneBit (int  i)  {         i |= (i >>  1 );     i |= (i >>  2 );     i |= (i >>  4 );     i |= (i >>  8 );     i |= (i >> 16 );     return  i - (i >>> 1 ); } 
寻找到最小的二次幂之后,重新计算阈值,然后用最小的2次幂来创建一个新的数组,最后用initHashSeedAsNeeded来初始化哈希掩码值。
第二步 1 2 if  (key == null )    return  putForNullKey(value); 
判断传入的key是不是null值,如果是就返回putForNullKey(value),不然就下一步。现在来看看这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private  V putForNullKey (V value)  {         for  (Entry<K,V> e = table[0 ]; e != null ; e = e.next) {         if  (e.key == null ) {             V  oldValue  =  e.value;             e.value = value;             e.recordAccess(this );             return  oldValue;         }     }     modCount++;         addEntry(0 , null , value, 0 );     return  null ; } 
第三步* 1 2 3 4 5 6 7 8 9 10 11 int  hash  =  hash(key);int  i  =  indexFor(hash, table.length);for  (Entry<K,V> e = table[i]; e != null ; e = e.next) {    Object k;     if  (e.hash == hash && ((k = e.key) == key || key.equals(k))) {         V  oldValue  =  e.value;         e.value = value;         e.recordAccess(this );         return  oldValue;     } } 
这步是最重要的一步,找到插入的位置然后将键值对插入到hashmap中。 首先就是调用hash方法来计算传入的key的哈希值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final  int  hash (Object k)  {    int  h  =  hashSeed;          if  (0  != h && k instanceof  String) {         return  sun.misc.Hashing.stringHash32((String) k);     }          h ^= k.hashCode();          h ^= (h >>> 20 ) ^ (h >>> 12 );     return  h ^ (h >>> 7 ) ^ (h >>> 4 ); } 
得到哈希值之后,就会调用indexFor方法来计算出应该插入的位置。
1 2 3 4 5 6 7 8 9 static  int  indexFor (int  h, int  length)  {                   return  h & (length-1 ); } 
计算出具体插入的位置之后就开始插入了,这里就是用了一个for循环遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for  (Entry<K,V> e = table[i]; e != null ; e = e.next) {    Object k;          if  (e.hash == hash && ((k = e.key) == key || key.equals(k))) {         V  oldValue  =  e.value;         e.value = value;         e.recordAccess(this );         return  oldValue;     } } modCount++; addEntry(hash, key, value, i); return  null ;
至此put的流程就结束了,到这里其实还是有问题。用indexFor这个方法来求数组的位置,刚刚也分析了这个方法,就是异或求余,那么就会有以下的问题,hash差为一定的倍数的时候,求余出来的下标就会是一样的,这个问题就是经常提到的哈希冲突。 为了解决哈希冲突这个问题,就引进了链表,也就是之前说的加入到桶中,这就是为什么哈希表底层是用数组和链表构成的了。下面讲讲大概是怎么用到链表的。
图中只是为了演示大概解决的过程,实现可以参考下面的createEntry方法和扩容中的transfer方法。由于哈希的计算会使数据尽量平均,而且扩容的时候又会重新计算下标,在数组长度较小的时候哈希冲突的发生的概率是很小的。
假设键值对3计算出来的i是3,就会被放到数组的第四个位置,直接放入。
假设键值对4计算出来的i是2,就会放到数组的第三个位置,但是这个位置已经有键值对1了,这种情况就会用到链表,头插到该链表。
但是如果是这种情况,就无法通过数组来寻找到键值对4了,所以还会变化一下指针的指向,最后就会变成这样。
addEntry 在上面put的第三步不难看到,在一个桶里只要有key相同,就覆盖,没有就使用addEntry这个方法加入到桶里。不难看出这个方法发生的情况,数组指定位置没有该元素或者是发生哈希冲突时就会用到这个方法,现在来看看这个方法。
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 void  addEntry (int  hash, K key, V value, int  bucketIndex)  {         if  ((size >= threshold) && (null  != table[bucketIndex])) {                  resize(2  * table.length);                  hash = (null  != key) ? hash(key) : 0 ;                  bucketIndex = indexFor(hash, table.length);     }          createEntry(hash, key, value, bucketIndex); } void  createEntry (int  hash, K key, V value, int  bucketIndex)  {    Entry<K,V> e = table[bucketIndex];          table[bucketIndex] = new  Entry <>(hash, key, value, e);     size++; } 
扩容 刚刚看到addEntry中用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 void  resize (int  newCapacity)  {         Entry[] oldTable = table;     int  oldCapacity  =  oldTable.length;     if  (oldCapacity == MAXIMUM_CAPACITY) {         threshold = Integer.MAX_VALUE;         return ;     }          Entry[] newTable = new  Entry [newCapacity];          transfer(newTable, initHashSeedAsNeeded(newCapacity));     table = newTable;          threshold = (int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 ); } 
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 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;         }     } } 
下面例子简化了长度,和一些细节,因为这样转移是很小几率的,只是为了展示转移方法的大概实现。
transfer方法和之前的createEntry方法,表明了1.7的哈希表为什么是头插的。因为头插,哈希表还会出现一些问题,之后还会提到。
get get方法就没有put需要考虑到这么多情况了了,所以简单很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public  V get (Object key)  {    if  (key == null )         return  getForNullKey();     Entry<K,V> entry = getEntry(key);     return  null  == entry ? null  : entry.getValue(); } 
第一步 1 2 if  (key == null )    return  getForNullKey(); 
首先就是判断key值是不是为null,如果是null就调用getForNullKey。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private  V getForNullKey ()  {         if  (size == 0 ) {         return  null ;     }          for  (Entry<K,V> e = table[0 ]; e != null ; e = e.next) {         if  (e.key == null )             return  e.value;     }          return  null ; } 
第二步 1 2 3 Entry<K,V> entry = getEntry(key); return  null  == entry ? null  : entry.getValue();
这两句也挺好理解的,用getEntry方法来获取键值对,然后返回value。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 final  Entry<K,V> getEntry (Object key)  {         if  (size == 0 ) {         return  null ;     }          int  hash  =  (key == null ) ? 0  : hash(key);          for  (Entry<K,V> e = table[indexFor(hash, table.length)];          e != null ;          e = e.next) {         Object k;         if  (e.hash == hash &&             ((k = e.key) == key || (key != null  && key.equals(k))))             return  e;     }     return  null ; } 
JDK8 JDK8优化了不少,但与JDK7在很多部分是一样的,所以我着重去分析一下不一样的地方。
参数和变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private  static  final  long  serialVersionUID  =  362498820763181265L ;static  final  int  DEFAULT_INITIAL_CAPACITY  =  1  << 4 ;static  final  int  MAXIMUM_CAPACITY  =  1  << 30 ;static  final  float  DEFAULT_LOAD_FACTOR  =  0.75f ;transient  int  size;transient  int  modCount;final  float  loadFactor;int  threshold;static  final  Entry<?,?>[] EMPTY_TABLE = {};transient  Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;static  final  int  ALTERNATIVE_HASHING_THRESHOLD_DEFAULT  =  Integer.MAX_VALUE;transient  int  hashSeed  =  0 ;private  transient  Set<Map.Entry<K,V>> entrySet = null ;transient  Node<K,V>[] table;transient  Set<Map.Entry<K,V>> entrySet;static  final  int  TREEIFY_THRESHOLD  =  8 ;static  final  int  UNTREEIFY_THRESHOLD  =  6 ;static  final  int  MIN_TREEIFY_CAPACITY  =  64 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 transient  Node<K,V>[] table;transient  Set<Map.Entry<K,V>> entrySet;static  final  int  TREEIFY_THRESHOLD  =  8 ;static  final  int  UNTREEIFY_THRESHOLD  =  6 ;static  final  int  MIN_TREEIFY_CAPACITY  =  64 ;
构造器 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 public  HashMap ()  {    this .loadFactor = DEFAULT_LOAD_FACTOR;  } public  HashMap (int  initialCapacity)  {    this (initialCapacity, DEFAULT_LOAD_FACTOR); } public  HashMap (int  initialCapacity, float  loadFactor)  {    if  (initialCapacity < 0 )         throw  new  IllegalArgumentException ("Illegal initial capacity: "  +                                            initialCapacity);     if  (initialCapacity > MAXIMUM_CAPACITY)         initialCapacity = MAXIMUM_CAPACITY;     if  (loadFactor <= 0  || Float.isNaN(loadFactor))         throw  new  IllegalArgumentException ("Illegal load factor: "  +                                            loadFactor);     this .loadFactor = loadFactor;          this .threshold = tableSizeFor(initialCapacity); } public  HashMap (Map<? extends K, ? extends V> m)  {    this .loadFactor = DEFAULT_LOAD_FACTOR;     putMapEntries(m, false ); } 
可以看出只有两个比较特殊的地方,那就是tableSizeFor和putMapEntries这两个方法了。
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 static  final  int  tableSizeFor (int  cap)  {    int  n  =  cap - 1 ;     n |= n >>> 1 ;     n |= n >>> 2 ;     n |= n >>> 4 ;     n |= n >>> 8 ;     n |= n >>> 16 ;          return  (n < 0 ) ? 1  : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; } final  void  putMapEntries (Map<? extends K, ? extends V> m, boolean  evict)  {    int  s  =  m.size();     if  (s > 0 ) {         if  (table == null ) {              float  ft  =  ((float )s / loadFactor) + 1.0F ;             int  t  =  ((ft < (float )MAXIMUM_CAPACITY) ?                      (int )ft : MAXIMUM_CAPACITY);             if  (t > threshold)                 threshold = tableSizeFor(t);         }         else  if  (s > threshold)             resize();         for  (Map.Entry<? extends  K , ? extends  V > e : m.entrySet()) {             K  key  =  e.getKey();             V  value  =  e.getValue();             putVal(hash(key), key, value, false , evict);         }     } } 
数据结构 JDK8的底层进行了一些优化,主体依旧是数组,链表为辅,但是链表过长会导致hashmap的查询效率变低 因为链表的查询操作的时间复杂度为O(n),所以在JDK8中引入了红黑树。 当链表达到树化阈值的时候,就会转变为红黑树 因为红黑树的查询操作的时间复杂度为O(log(n)),所以可以提高查询桶里节点的效率