HashMap是基于Map接口的一个非同步实现,此实现提供key-value形式的数据映射,支持null值。node
HashMap的常量和重要变量以下:算法
DEFAULT_INITIAL_CAPACITY = 16
|
Node数组的默认长度
|
MAXIMUM_CAPACITY = 1073741824
|
Node数组的最大长度
|
DEFAULT_LOAD_FACTOR = 0.75F
|
负载因子,调控控件与冲突率的因数
|
TREEIFY_THRESHOLD = 8
|
链表转换为树的阈值,超过这个长度的链表会被转换为红黑树
|
UNTREEIFY_THRESHOLD = 6
|
当进行resize操做时,小于这个长度的树会被转换为链表
|
MIN_TREEIFY_CAPACITY = 64
|
链表被转换成树形的最小容量,若是没有达到这个容量只会执行resize进行扩容
|
Node<K, V>[] table
|
储存元素的数组
|
Set<Map.Entry<K, V>> entrySet
|
set数组,用于迭代元素
|
int size
|
存放元素的个数,但不等于数组的长度
|
int modCount
|
每次扩容和更改map结构的计数器
|
int threshold
|
临界值,当实际大小(容量*负载因子)超过临界值的时候,会进行扩容
|
float loadFactor
|
负载因子,默认为0.75F
|
在jdk1.8中,HashMap是采用数组+链表+红黑树的形式实现。以下图: 数组
其中链表的实现以下:安全
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
能够看到,node中包含一个next变量,这个就是链表的关键点,hash结果相同的元素就是经过这个next进行关联的。性能
接下来看看红黑树的实现:this
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }
......
}
红黑树比链表多了四个变量,parent父节点、left左节点、right右节点、prev上一个同级节点,红黑树内容较多,有兴趣的能够自行百度,不在赘述。spa
在来讲说hash算法,HashMap中使用的算法以下线程
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这个hash先将key右移了16位,而后与key进行异或,这里还涉及到后面put方法中的另外一次&操做,code
tab[i = (n - 1) & hash]
tab既是table,n是map集合的容量大小,hash是上面方法的返回值。由于一般声明map集合时不会指定大小,或者初始化的时候就建立一个容量很大的map对象,因此这个经过容量大小与key值进行hash的算法在开始的时候只会对低位进行计算,虽然容量的2进制高位一开始都是0,可是key的2进制高位一般是有值的,所以先在hash方法中将key的hashCode右移16位在与自身异或,使得高位也能够参与hash,更大程度上减小了碰撞率。对象
构造方法以下:
public HashMap() //默认构造方法 public HashMap(int initialCapacity)//参数为初始大小 public HashMap(int initialCapacity, float loadFactor)//参数为初始大小,负载因子
这里又涉及到另外一个算法:
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; }
这个算法颇有意思,经过给定的大小cap,计算大于等于cap的最小的2的幂数。连续5次右移运算乍一看没有什么意思,但仔细一想2进制都是0和1啊,这就有问题了,第一次右移一位,就表示但凡是1的位置右边的一位都变成了1,第二次右移两位,上次已经把有1的位置都变成连续两个1了,是否是感受很神奇,如此下来5次运算正好将int的32位都转了个遍,以最高的一个1的位置为基准将后面全部位数都变为1,而后在进行n+1,不就变成了2的幂数。这里还有一点要注意的是第一行的cap-1,这是由于若是cap自己就是2的幂数,会出现结果是cap的2倍的状况,会浪费空间。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //put方法的实际执行者
//hash,key的hash值;onlyIfAbsent,是否改变原有值;evict,LinkedHashMap回调时才会用到。
Node<K,V>[] tab;
Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //table为空或长度为0时,对table进行初始化,分配内存 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //当put的key在map中不存在时,直接new一个Node存入table。 else { //当key在map中存在时,进入此分支。 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //此处的p是table中的第n-1个元素,每一次必检查此元素的key是否与put的key相同,若是相同则替换value e = p; else if (p instanceof TreeNode) //检查p是不是TreeNode类型。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若是第n-1个元素不符合,则一次遍历table中其他元素,直到找到相应key值。 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //此处主要为了防止hash出现重复,只有上边tab[i = (n - 1) & hash]中存在元素且不是put的元素时才会进入这个分支。 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 此处判断的意义是当链表长度超过8时,转换为红黑树,在1.8之前是没有的,链表查询的复杂度是O(n),而红黑树是O(log(n)),可是若是hash结果不均匀会极大的影响性能 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //进入此分支标志已找到与put的key值相同的元素,元素变量为e。 break; p = e; } } if (e != null) { // 将对应key的value替换为put的value,同时返回旧value,只有存在key时才会返回! V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); //对table进行扩容 afterNodeInsertion(evict); return null; }
从上面的源码能够看出,首先会判断key的hash与map容量-1的与计算值,若是数组中这个位置没有元素则直接插入,反之则在遍历此位置的元素链表,直到在最后插入。这个地方有一个链表的阈值(默认是8),若是一个链表的长度达到了阈值,则调用treefyBin()将链表转换成红黑树。
final Node<K,V>[] resize() { //扩容方法
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //map如今的容量
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; // 若是如今的容量扩大为2倍依然没有超过最大容量,而且如今的容量大于等于数组的默认长度,将map的容量扩大为2倍 } else if (oldThr > 0) //hashMap有一个构造方法会指定初始大小,这时候就会用到这个分支,对map进行初始化 newCap = oldThr; else { //这个分支是默认的初始化参数分支,好比用new hashMap()生成一个对象,就会进入这个分支初始化 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) { //扩容操做,将oldTab的元素依次添加到newTab 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 { // preserve order 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; }
在这个方法里有几个比较有意思的地方,首先是最大容量MAXIMUN_CAPACITY,这个值实际就是int的最大值,我的推测应该是为了配合put时的hash算法,由于计算key值hash的算法返回值是int型,若是容量超过int的阈值,在进行与运算时碰撞率会增大不少倍。而后是扩容的方式,这里是new了一个数组!这也是HashMap不安全的关键之一。当超过一个线程对一个HashMap对象进行put的时候若是触发了resize方法,后执行的那一个会把以前执行的结果覆盖掉!也就是先put的值不会真的存入map中。
final void treeifyBin(Node<K,V>[] tab, int hash) { //将链表转换为红黑树 int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //若是map的容量小于64(默认值),会调用resize扩容,不会转换为红黑树 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); //Node转换为TreeNode if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); //调用TreeNode的树排序方法 } }
这里须要注意的是当map容量小于64时,就算链表超过了8也不会转换为红黑树。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //主要用于clone方法和putAll方法和一个构造方法HashMap(Map<? extends K, ? extends V> m)。 int s = m.size(); if (s > 0) { if (table == null) { // 若是是一个new的map,即table变量尚未初始化,先经过参数的map.size和负载因子计算所需的map大小。 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) //若是计算出的大小大于map的实际存储容量,则从新计算存储容量 threshold = tableSizeFor(t); } else if (s > threshold) //若是参数map大小大于实际存储容量,则将map容量变为2倍 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); } } }
final Node<K,V> getNode(int hash, Object key) { //get的实际实现方法
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) { //经过hash定位到一个桶,而且有元素存在 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //老是检查桶中的第一个元素 return first; if ((e = first.next) != null) { //若是第一个元素不符合,则继续搜索 if (first instanceof TreeNode) //若是桶中是红黑树,进入此分支,实际执行的是TreeNode中的find方法,从根节点开始向下搜寻 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; }
看过put方法后再来看get就容易不少了,首先用put中的hash算法定位到数组中的位置,若是有元素且key值相同直接返回,若是有元素且key值不一样那就要向下遍历了,若是是链表就一直遍历next,若是是红黑树则调用getTreeNode方法查找节点。
abstract class HashIterator { //KeySet和EntrySet的迭代器的父类 Node<K,V> next; // 下一个元素 Node<K,V> current; // 删除方法会调用 int expectedModCount; // 就是hashMap中的modCount int index; // 元素序号 HashIterator() { //为上面四个变量赋初始值 expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } ....... }
HashIterator是KerSet和EntrySet的父类,这个迭代器中有一个属性是expectedModCount要特别注意,这个变量等价于HashMap对象中的modCount,若是在迭代器没有执行完的期间对map进行增删,会抛出异常阻止继续迭代,代码以下:
final Node<K,V> nextNode() { //获取下一个元素 Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) //在迭代器执行期间只能用下面的remove删除元素,不然会抛出异常,不能增长元素 throw new ConcurrentModificationException(); if (e == null) //若是没有下一个会抛出异常 throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { //若是next没有取到值,经过index继续向下遍历,直到取到元素为止 do {} while (index < t.length && (next = t[index++]) == null); } return e; }
不过迭代器虽然不容许添加元素,可是给了一个删除的方法:
public final void remove() { //删除方法 Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; //关键语句,这里作了一个同步,不然与正常的删除没有区别,这个同步避免了上面的异常抛出 }
与普通的删除方法就差在最后这一行上,这里同步了迭代器和HashMap对象的操做数,便不会抛出异常