HashMap
HashMap是是经典的数据结构, 也是Java最常用的数据结构之一. 由于HashMap不涉及多线程问题, 且作者 因此代码理解难度比较低, 非常值得一读.
HashMap的源码真的有一种作者希望我能看懂的感觉
- 如果多个Key具有同样的Hash值, 但Key本身是Comparable的, 那么将通过比较的方式减少Key冲突造成的
计算哈希值
1 2 3 4
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
将对象的哈希值与高16位做异或操作, 从而保证高位的bit能够影响到低位的bit. 由于HashMap只根据当前table的大小是否部分的低位bit,因此这一操作有助于提高Hash函数的不一致性, 减少冲突.
注意在表达式中赋值的用法, 为了简化代码, HashMap中大量使用了此技巧
构造函数
1 2 3 4 5 6 7 8 9 10 11 12
| 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); }
|
构造函数中是一些检查操作. 注意, 此时并没有分配任何的内存, 因为HashMap采取了惰性加载机制, 只有真的开始使用HashMap的时候才会分配内存.
添加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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); } } }
|
当实际向HashMap中添加数据后, 进入此方法. 此方法实际上包含两个逻辑分支, 即如果还没有初始化(table == null
)或者需要扩容(s > threshold
)则调用对应的方法进行初始化或者扩容. 否则正常的添加数据.
扩容操作
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
| 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; } else if (oldThr > 0) newCap = oldThr; else { 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) { 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 { 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; }
|
第一段if-else语句重新计算了容量. 这一段是按照出现概率写的, 即普通的扩容操作最常见,因此在第一个if中处理, 而初始化操作只会执行一次, 因此最后处理.
扩容操作很简单, 根据节点类型进行分裂即可. 由于HashMap的大小始终是2的幂, 因此最需要按照哈希值的最高位重新分布即可.
参考资料
以下的几篇参考资料中, 第一篇重点分析了HashMap的插入过程, 解释了其中使用的一些二进制技巧, 并且提供了插入过程的流程图. 第二篇重点介绍了插入过程中, 红黑树的操作, 从红黑树的基本操作开始, 详细介绍了HashMap的红黑树创建和插入过程.
第三篇完整的介绍了HashMap的插入, 删除过程, 对源代码的注解比较详细.第四篇文章在对源代码进行注释的同时, 还提供了操作逻辑的总结, 看文字总结更容易理解.
第五篇介绍了JDK1.7中为什么HashMap会产生死循环.
以下几篇参考资料中, 第一篇介绍了红黑树的插入和删除过程, 不过全程使用伪代码描述, 直接看可能看不懂.
JDK类库源码分析
多线程环境DEBUG
可以对所有线程在指定的位置进行断点, 所以即使是多线程环境, 也能够一步步的执行代码
JDK集合类归纳