对于写了这么久的HashMap,对其知之甚少,趁大三有点闲时,打算深入了解下HashMap的原理。
- HashMap、HashTable之间有什么关系
- HashMap在put、remove的时候做了什么
- resize(再哈希)是什么
- 为什么每次扩容,容量都是原来的二次方
HashMap和HashTable间的关系
HashMap和HashTable间的差异:
- HashMap是线程不安全的,HashTable几乎都加上了方法级别的Synchronize,所以同一时间最多只有一个线程可以对同个方法进行调用
HashMap允许null键和null值,HashTable遇到null值会抛出NPE
HashTable的源码分析戳这里
HashMap再put、remove的时候做了什么
JDK6
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 101
|
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; } 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; } void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } 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); table = newTable; threshold = (int)(newCapacity * loadFactor); } void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
|
JDK7
在JDK6的基础上,没有做特别的改变,倒是去除了modCount的volatile关键字(1.7取消了modCount的volatile)。
移除volatile的原因:
对于单线程集合,没必要承担volatile的额外消耗;多线程情况下,也不应该使用单线程集合,所以在hashmap里面移除volatile
移除Volatile的原文
JDK8
需要红黑树的知识,待学习
再哈希
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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); table = newTable; threshold = (int)(newCapacity * loadFactor); }
|
每次扩容,容量都是原来的二次方
容量都是2的N次,主要是为了下面的% length```,先保证不超过最大长度,其次保证hash坐落的位置。但是取模运算是所有运算符里较为繁杂的运算指令。大师们为了更高效,就不得不采用位运算符并让```hash```的每位都参与到运算中。这里放出式子```hash & (length - 1)```1 2 3 4 5 6 7 8
| ```java /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
|
可以看一下,如果容量的长度不是2的幂次方
1 2 3
| length: 1000 0000 & hash: 0110 1111
|
上述例子实际可存放的位置就只有一个: 1000 0000 ,会有大量的空间被浪费,
length: 1111 1111
&
hash: 0110 1111
1
| 但是这样的容量很难通过一次移位获取,所以实际情况应该是2的N次方 - 1,尽可能的将1布满。如下所示
|
length: 0111 1111
&
hash: 0110 1111
`
通过移位获取最大的2的N次方,减去1,获得最可能的理想情况,允许存在128个位置。一个是256,一个是255,前者只能存放1个位置,后者可以存放128个位置,其高效可想而知。
所以容量是2的幂次方是为了减少空间的浪费,降低hash冲突的几率