HashMap源码分析
2017-11-27 16:14
134 查看
HashSet、HashMap
概述
分别实现了Set和Map接口,底层用哈希表来存储,迭代时是无序的。add、remove、contains、size方法是常数时间的,遍历是O(N+M)的(N表示桶数,M表示元素个数)。
都是非线程安全的,类似的也可以用Collections.synchronizedSet转化为线程安全的容器。
HashSet实质上也是基于HashMap来实现的,只是map中的value为static final的object,因此主要分析HashMap即可。
影响HashMap性能的主要有两个参数,初始容量(capacity)和装载因子(load factor)。capacity决定初始时桶的数量,要求是2的倍数,默认是16;loadfactor决定了哈希表能装多满才进行扩容,默认是0.75。
哈希表是由链表数组实现的,每个链表的元素超过阈值(TREEIFY_THRESHOLD = 8)时会将对应的链表转化为红黑树结构,来提高查找效率。当低于另一个阈值时(UNTREEIFY_THRESHOLD = 6)又会转化回链表结构。
树桶(Tree bins)的节点是内部的TreeNode类,按照红黑树的结构组织,内部有treeify方法将链节点转化为树,也有untreeify方法将树转化为链节点。链桶的节点时内部的Node类,就是普通的单向链表。
源码分析
构造器public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } 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); } // 计算大于等于cap的2的n次方数,其实就是通过为运算来加速 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; }
构造器逻辑比较简单,就是判断一些参数合法性后进行设置。桶的容量设为2的幂是为了提高查找的效率,对于普通的数查找对应的hash桶时一般是通过取余来映射,如果是2的幂,通过h&(capacity-1)的位运算即可实现,这种方式显然效率更高。
2. hash
hash方法用于HashMap内部对对象的hashcode的处理,能提高散列效果:
// HashMap的桶是2的幂,如果直接取余,当capacity较少,hashcode的低位 // 比较相似时,会产生较多冲突。这里通过右移16位再异或,这种方式能更 // 好地利用hashcode的高位,使散列更均匀,且时间复杂度也较低 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
resize
resize方法是初始化或者再散列时调用的方法,是HashMap中比较核心的一个方法:
// 这个方法在初始化或者扩容时会调用。初始化时,会根据设定的capacity来分配桶。 // 扩容时,由于桶数都是2的幂,因此扩容后原来的节点索引可能不变,也可能移动2的幂次位, // 如capacity=4,5%4=1,3%4=3,扩容后capacity=8,5%8=5,移动了4位,3%8=3,不变 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; 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; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold // 初始化,使用设定的threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 初始化,使用默认的threshold(16) 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; // 上面这部分代码其实都是计算resize后的capacity和threshold @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 这里开始是扩容后的元素处理逻辑 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 这里将原表指向该元素的引用设为null,可以避免内存泄漏 oldTab[j] = null; if (e.next == null) // next为空说明该桶只有单个节点 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 如果节点是TreeNode,则进行切分 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 如果是链表,也要按情况进行拆分,参考split方法,思路是类似的 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; } // 这个方法是TreeNode的实例方法,由HashMap的resize方法内部调用。将树桶切分成“高位桶” // 和“低位桶”,并且切分完后根据节点数进行treeify或untreeify。 // 为什么切分成“高位桶”和“低位桶”?结合HashMap的特性考虑,capacity都是2的幂,映射到的桶 // 的索引实质上就是看key最右边的几位。表扩容后是扩为原来的2倍,即1右移了一位,扩容后映 // 射到的新索引只有两种情况,只需看新的那一位是不是0,如果为0,说明新表中索引不变,加入 // “低位桶”,如果为1,说明索引移动了k位,加入“高位桶” final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // 分别保存“高位桶”和“低位桶”的首尾节点 TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; for (TreeNode<K,V> e = b, next; e != null; e = next) { // 遍历节点,按照hash&bit是否为0,分别加入两个桶 next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } // 后面就是根据节点数来进行treeify或者untreeify了 if (loHead != null) { if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
列了两个主要的相关方法,虽然很长,但看看注释理清思路就差不多了。这里没有给出TreeNode的treeify和untreeify的具体代码,其实就是红黑树和链表的一些相关操作了,大概知道原理即可。
另外也可以看到,这里都是线程不安全的,又有很多链表操作,多线程环境来修改的话,可能会造成一些意外结构,形成死循环,相关的内容网上也很容易看到。
4. put
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // put的核心方法,返回原key对应的value或者null 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) // 第一次put时会走这个逻辑 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) // 如果对应的桶为空,直接添加到该桶即可 tab[i] = newNode(hash, key, value, null); else { // 否则,在该桶里找到key对应的节点,或者新建节点 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 如果是树节点,则要调用树的相应方法进行添加 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 遍历链表来查找 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 超过阈值要进行treeify treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key // 如果不为空,说明存在该key对应的节点,则进行更新 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 这是回调方法 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) // 如果添加元素后超过阈值,要进行resize resize(); // 这是回调方法 afterNodeInsertion(evict); return null; }
虽然也挺长,但方法的流程还是比较清晰的,就是啰嗦了点。
使用HashMap时,需要注意作为key的类的equals和hashcode要保持一致,即如果两个对象是equal的,则也应该有相同的hashcode。这从put的代码就可以看出来原因。一个key是根据hashcode来映射的,如果两个对象在业务上应该是equal的,但有不同的hashcode,则节点可能在不同的桶中,必然会造成混乱。
4. get
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; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) // 如果是树节点,则调用树的get进行查找,其实也就是红黑树的查找逻辑了 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; }
get相对而言就比较简单了,如果是链表直接遍历即可,如果是树节点,就要调用TreeNode的方法来get了,其实就是红黑树的查找逻辑了。
另外还有HashMap的remove方法,整体逻辑跟get其实很相似,只是最后不是更新value,而是一些引用的调整。
总结
上面就把HashMap的比较重要的方法源码过了一遍,整体上还是比较清晰的,跟经典的HashMap实现方法也比较一致。1.8开始HashMap才加入了TreeNode的结构,来改善单个桶链表很长时的查找性能,而且还简化了其他的一些细节,感觉更容易理解。
HashMap的一些实现细节还是很值得学习的,比如对capacity限制为2的倍数,这在mapping、resize时都可以通过位运算来提高效率!
类库源码比较注重效率,所以可能有些地方比较臃肿和冗余,重点应该学习算法思想和组织框架。
相关文章推荐
- Map集合及HashMap源码分析
- 源码分析-HashMap
- 从JDK源码分析HashMap
- java源码分析--HashMap的工作原理
- HashMap源码分析
- java8-HashMap源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- HashMap源码分析
- HashMap的源码分析与实现 伸缩性角度看hashmap的不足
- HashMap源码分析
- Java集合之HashMap源码实现分析
- Hashmap和hashtable三大区别(从源码角度分析为什么map可以存放一个key为null,多个值为null)的特点
- HashMap源码分析
- Java 集合框架源码分析(三)——HashMap
- java中hashmap源码分析和实现
- Java源码分析之HashMap
- HashMap源码分析与实现
- 源码分析四(HashMap与HashTable的区别 )
- HashMap 源码分析
- HashMap实现原理及源码分析