笔者以前看过一篇关于jdk1.8的HashMap源码分析,做者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐。笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,还添加了不少其余补充内容。html
如下是HashMap的数据结构:java
不一样于以前的jdk的实现,1.8采用的是数组+链表+红黑树,在链表过长的时候能够经过转换成红黑树提高访问性能。大多数状况下,结构都以链表的形式存在,因此检查是否存在树节点会增长访问方法的时间,可是相较于其优势来讲仍是能够接受的。特别说明:树结构里还有不少指针引用,这里没画出来。将在后续的LinkedHashMap和TreeMap中讲解node
能够看到HashMap继承自AbstractMap,实现了Serializable和Cloneable。这里笔者不打算介绍AbstractMap的源码,由于阅读以后发现比较简单,有兴趣的园友们能够自行去看看,其中的keyset()
和values()
方法与HashMap中的相似。Serializable接口表示HashMap实现了的序列化,Cloneable接口表示能够合法的调用clone()
,若是不实现该接口而调用clone,会报CloneNotSupportedException。关于Map接口的解析,能够看我以前的文章编程
下面咱们先来看一下HashMap里面的成员变量:数组
//默认初始化map的容量:16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //map的最大容量:2^30 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的填充因子:0.75,能较好的平衡时间与空间的消耗 static final float DEFAULT_LOAD_FACTOR = 0.75f; //将链表(桶)转化成红黑树的临界值 static final int TREEIFY_THRESHOLD = 8; //将红黑树转成链表(桶)的临界值 static final int UNTREEIFY_THRESHOLD = 6; //转变成树的table的最小容量,小于该值则不会进行树化 static final int MIN_TREEIFY_CAPACITY = 64; //上图所示的数组,长度老是2的幂次 transient Node<K,V>[] table; //map中的键值对集合 transient Set<Map.Entry<K,V>> entrySet; //map中键值对的数量 transient int size; //用于统计map修改次数的计数器,用于fail-fast抛出ConcurrentModificationException transient int modCount; //大于该阈值,则从新进行扩容,threshold = capacity(table.length) * load factor int threshold; //填充因子 final float loadFactor;
能够看到,HashMap里是以Node节点数组的形式存放数据的,Node数据结构比较简单,这里咱们也来看一下:数据结构
//Entry接口在笔者的总章里有介绍。 static class Node<K,V> implements Map.Entry<K,V> { // key & value 的 hash值 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; } }
因为比较简单,这里就不详细介绍了哈。
并发
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; //tableSizeFor()是用来将初始化容量转化大于输入参数且最近的2的整数次幂的数,好比initialCapacity = 7,那么转化后就是8。 this.threshold = tableSizeFor(initialCapacity); }
tableSizeFor(),将初始化容量转化大于或等于最接近输入参数的2的整数次幂的数:函数
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; }
|
是或运算符,好比说0100 | 0011 = 0111
,>>>
是无符号右移,忽略符号位,空位都以0补齐,好比说0100 >>> 2 = 0001
,如今来讲一下这么作的目的:
源码分析
首先>>>
和|
的操做的目的就是把n从最高位的1如下都填充为1,以010011为例,010011 >>> 1 = 001001
,而后001001 | 010011 = 011011
,而后再把011011无符号右移两位:011011 >>> 2 = 000110
,而后000110 | 011011 = 011111
,后面的四、八、16计算过程就都省去了,int类型为32位,因此计算到16就所有结束了,最终获得的就是最高位及其如下的都为1,这样就能保证获得的结果确定大于或等于原来的n且为奇数,最后再加上1,那么确定是:大于且最接近输入值的2的整数次幂的数。
那么为何要先cap - 1
呢,咱们能够先思考如下,若是传进来的自己就是2的整数幂次,好比说01000
,10进制是8,那么若是不减,获得的结果就是16,显然不对。因此先减1的目的是cap若是刚好是2的整数次幂,那么返回的也是自己。
合起来获得这个tableSizeFor()方法的目的:返回大于或等于最接近输入参数的2的整数次幂的数。另外,笔者特地回去看了JDK1.7的源码,发现1.7用的是roundUpToPowerOf2()
方法,里面用到里了>>
以及减操做,性能上来讲确定还1.8的高。布局
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
putMapEntries():
//evict表示是否是初始化map,false表示是初始化map final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //获取m中键值对的数量 int s = m.size(); if (s > 0) { if (table == null) { //计算map的容量,键值对的数量 = 容量 * 填充因子 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //若是容量大于了阈值,则从新计算阈值。 if (t > threshold) threshold = tableSizeFor(t); } //若是table已经有,且键值对数量大于了阈值,进行扩容 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); } } }
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //先是判断一通table是否为空以及根据hash找到存放的table数组的下标,并赋值给临时变量 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //老是先检查数组下标第一个节点是否知足key,知足则返回 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; //若是第一个与key不相等,则循环查看桶 if ((e = first.next) != null) { //检查是否为树节点,是的话采用树节点的方法来获取对应的key的值 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //do-while循环判断,直到找到为止 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
能够发现源码做者很喜欢在判断的时候赋值,不知道这个是否是个编程的好习惯。!?(・_・;?
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * @param hash key的hash值 * @param key * @param value * @param onlyIfAbsent 若是为true,则在有值的时候不会更新 * @param evict false表示在建立map */ 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; //若是tab对应的数组位置为空,则建立新的node,并指向它 if ((p = tab[i = (n - 1) & hash]) == null) // newNode方法就是返回Node:return new Node<>(hash, key, value, next); tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //若是比较hash值和key的值都相等,说明要put的键值对已经在里面,赋值给e if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若是p节点是树节点,则执行插入树的操做 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //不是树节点且数组中第一个也不是,则在桶中查找 else { for (int binCount = 0; ; ++binCount) { //找到了最后一个都不知足的话,则在最后插入节点。注意这里的e = p.next,赋值兼具判断都在if里了 if ((e = p.next) == null) p.next = newNode(hash, key, value, null); //以前field说明中的,若是桶中的数量大于树化阈值,则转化成树,第一个是-1 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } //在桶中找到了对应的key,赋值给e,退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //没有找到,则继续向下一个节点寻找 p = e; } } //上面循环中找到了e,则根据onlyIfAbsent是否为true来决定是否替换旧值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //钩子函数,用于给LinkedHashMap继承后使用,在HashMap里是空的 afterNodeAccess(e); return oldValue; } } //修改计数器+1 ++modCount; //实际大小+1, 若是大于阈值,从新计算并扩容 if (++size > threshold) resize(); //钩子函数,用于给LinkedHashMap继承后使用,在HashMap里是空的 afterNodeInsertion(evict); return null; }
能够看到真正执行put的是里面的putVal()
方法。里面的插入逻辑一步步下来仍是很清晰的。
经过调用resize()
对map进行扩容操做。
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //扩容/缩容前的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧的阈值 int oldThr = threshold; int newCap, newThr = 0; //说明以前已经初始化过map if (oldCap > 0) { //达到了最大的容量,则将阈值设为最大,而且返回旧的table if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //若是两倍的旧容量小于最大的容量且旧容量大于等于默认初始化容量,则旧的阈值也扩大两倍。 //oldCap << 1,其实就是*2的意思。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //旧容量为0且旧阈值大于0,则赋值给新的容量(应该是针对初始化的时候指定了其容量的构造函数出现的这种状况) else if (oldThr > 0) newCap = oldThr; //这种状况就是调用无参数的构造函数 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新阈值为0,则经过:新容量*填充因子 来计算 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //根据新的容量来初始化table,并赋值给table @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //若是旧的table里面有存放节点,则初始化给新的table if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //将下标为j的数组赋给临时节点e if ((e = oldTab[j]) != null) { //清空 oldTab[j] = null; //若是该节点没有指向下一个节点,则直接经过计算hash和新的容量来肯定新的下标,并指向e if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若是为树节点,按照树节点的来拆分 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //e还有其余的节点,将该桶拆分红两份(不必定均分) else { //loHead是拆分后的,链表的头部,tail为尾部 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //根据e的hash值和旧的容量作位与运算是否为0来拆分,注意以前是 e.hash & (oldCap - 1) 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; }
能够看到,resize()
方法对整个数组以及桶进行了遍历,极其耗费性能,因此再次强调在咱们明确知道map要用的容量的时候,使用指定初始化容量的构造函数。
在resize前和resize后的元素布局以下:
再次强调一下,拆分后的结果不必定是均分,要看你存的值
public V remove(Object key) { Node<K,V> e; //与以前的put、get同样,remove也是调用其余的方法 return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } /** * Implements Map.remove and related methods * * @param hash key的hash值 * @param key * @param value 与下面的matchValue结合,若是matchValue为false,则忽略value * @param matchValue 为true,则判断是否与value相等 * @param movable 主要跟树节点的remove有关,为false,则不移动其余的树节点 */ 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; //老规矩,仍是先判断table是否为空之类的逻辑,注意赋值操做 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-while循环查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //若是找到了key对应的node,则进行删除操做 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); //若是p == node,说明该key所在的位置为数组的下标位置,因此下标位置指向下一个节点便可 else if (node == p) tab[index] = node.next; //不然的话,key在桶中,p为node的上一个节点,p.next指向node.next便可 else p.next = node.next; //修改计数器 ++modCount; --size; //钩子函数,与上同 afterNodeRemoval(node); return node; } } return null; }
这里提到里的remove的话,确定与之联想到的就是其抛出ConcurrentModificationException
。举个栗子:
Map<String, Integer> map = new HashMap<>(); map.put("GoddessY", 1); map.put("Joemsu", 2); for (String a : map.keySet()) { if ("GoddessY".equals(a)) { map.remove(a); } }
这里咱们再来看一下其在循环过程当中抛出该异常的源码(以keySet()
为例):
public Set<K> keySet() { Set<K> ks; return (ks = keySet) == null ? (keySet = new KeySet()) : ks; } final class KeySet extends AbstractSet<K> { public final Iterator<K> iterator() { return new KeyIterator(); } } final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } abstract class HashIterator { //指向下一个节点 Node<K,V> next; //指向当前节点 Node<K,V> current; //迭代前的修改次数 int expectedModCount; //当前下标 int index; HashIterator() { //注意这里:将修改计数器值赋给expectedModCount expectedModCount = modCount; //下面一顿初始化。。。 Node<K,V>[] t = table; current = next = null; index = 0; //在table数组中找到第一个下标不为空的节点。 if (t != null && size > 0) { do {} while (index < t.length && (next = t[index++]) == null); } } //经过判断next是否为空,来决定是否hasNext() public final boolean hasNext() { return next != null; } //这里就是抛出ConcurrentModificationException的地方 final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; //若是modCount与初始化传进去的modCount不一样,则抛出并发修改的异常 if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); //若是一个下标对应的桶空了,则接着在数组里找其余下标不为空的桶,同时赋值给next if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } //使用迭代器的remove不会抛出ConcurrentModificationException异常,缘由以下: 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从新进行了赋值。因此下次比较的时候仍是相同的 expectedModCount = modCount; } }
那么咱们再回到上面的测试代码,咱们再来看一个有趣的问题,若是我把"GoddessY".equals(a)
换成"Joemsu".equals(a)
还会抛出异常吗?有兴趣的园友们能够试一试,找出缘由可以加深对源码的理解!(づ。◕‿‿◕。)づ
最后咱们再来看一下将桶变成红黑树的代码吧,具体的树结构之类的大概会放在TreeMap里讲解,这里不仔细介绍。
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //这里MIN_TREEIFY_CAPACITY派上了用场,及时单个桶数量达到了树化的阈值,总的容量没到,也不会进行树化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { // 返回树节点 return new TreeNode<>(p.hash, p.key, p.value, next); TreeNode<K,V> p = replacementTreeNode(e, null); //为空说明是第一个节点,做为树的根节点 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); } }
下面是一些关于HashMap的特征:
容许key和value为null
基本上和Hashtable(已弃用)类似,除了非同步以及键值能够为null
不能保证顺序
访问集合的时间与map的容量和键值对的大小成比例
影响HashMap性能的两个变量:填充因子和初始化容量
一般来讲,默认的填充由于0.75是一个时间和空间消耗的良好平衡。较高的填充由于减小了空间的消耗,可是增长了查找的时间
最好可以在建立HashMap的时候指定其容量,这样能存储效率比使其存储空间不够后自动增加更高。毕竟从新调整耗费性能
使用大量具备相同hashcode值的key,将下降hash表的表现,最好能实现key的comparable
注意hashmap是不一样步的。若是要同步请使用Map m = Collections.synchronizedMap(new HashMap(...));
除了使用迭代器的remove方法外其的其余方式删除,都会抛出ConcurrentModificationException.
map一般状况下都是hash桶结构,可是当桶太大的时候,会转换成红黑树,能够增长在桶太大状况下访问效率,可是大多数状况下,结构都以桶的形式存在,因此检查是否存在树节点会增长访问方法的时间
最后谢谢各位园友观看,若是有描述不对的地方欢迎指正,与你们共同进步!