简介 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)),所以可以提高查询桶里节点的效率