文章仅是笔者个人的学习笔记,存在一些只有笔者个人能看到的用词或者描述,如果有不明确的地方,欢迎留言,理性讨论。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ ... }
哈希和拉链法
![](https://www.www.zyiz.net/i/ll/?i=img_convert/f35ad2e4663b428d41573268e79ab38d.png#align=left&display=inline&height=249&margin=[object Object]&originHeight=470&originWidth=809&size=0&status=done&style=none&width=428)
链表与红黑树
哈希位置定位
// 代码1 static final int hash(Object key) { // 计算key的hash值 int h; // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // 代码2 int n = tab.length; // 将(tab.length - 1) 与 hash值进行&运算 int index = (n - 1) & hash;
- ~~(这部分算法感觉可以单独写一篇来学了,实际原理有点复杂啊~~ - 不行,就是干!冲就完事了,一定要搞懂
//以下是 jdk 1.8 以下的方法! public HashMap() { //负载因子:用于衡量的是一个散列表的空间的使用程度,默认0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; //HashMap进行扩容的阈值,它的值等于 HashMap 的容量,默认16,乘以负载因子 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); // HashMap的底层实现仍是数组,只是数组的每一项都是一条链 table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } //以下是 jdk1.8的方法! 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); }
- 可以传入自定义的初始大小和负载因子,不过需要注意: - jdk 1.8 之前,构造方法中就进行了 数组的初始化,但是 1.8 开始,只是记录初始容量和负载因子的值,到第一次put的时候才会真正去初始化数组,等于是有懒加载的机制 - 初始容量和负载因子对HashMap性能的影响是非常大的,对于 拉链法 的哈希表(jdk 1.7及以下是纯链表),查找一个元素的平均时间是 O(1+a),a 指的是链的长度,是一个常数。若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低
/** * 测试目的:理解HashMap发生resize扩容的时候对于链表的优化处理: * 初始化一个长度为8的HashMap,因此threshold为6,所以当添加第7个数据的时候会发生扩容; * Map的Key为Integer,因为整数型的hash等于自身; * 由于hashMap是根据hash &(n - 1)来确定key所在的数组下标位置的,因此根据公式 m(m >= 1)* capacity + hash碰撞的数组索引下标index,可以拿到一组发生hash碰撞的数据; * 例如本例子capacity = 8, index = 7,数据为:15,23,31,39,47,55,63; * 有兴趣的读者,可以自己动手过后选择一组不同的数据样本进行测试。 * 根据hash &(n - 1), n = 8 二进制1000 扩容后 n = 16 二进制10000, 当8的时候由后3位决定位置,16由后4位。 * * n - 1 : 0111 & index resize--> 1111 & index * 15 : 1111 = 0111 resize--> 1111 = 1111 * 23 : 10111 = 0111 resize--> 10111 = 0111 * 31 : 11111 = 0111 resize--> 11111 = 1111 * 39 : 100111 = 0111 resize--> 100111 = 0111 * 47 : 101111 = 0111 resize--> 101111 = 1111 * 55 : 110111 = 0111 resize--> 110111 = 0111 * 63 : 111111 = 0111 resize--> 111111 = 1111 * * 按照传统的方式扩容的话那么需要去遍历链表,然后跟put的时候一样对比key,==,equals,最后再放入新的索引位置; * 但是从上面数据可以发现原先所有的数据都落在了7的位置上,当发生扩容时候只有15,31,47,63需要移动(index发生了变化),其他的不需要移动; * 那么如何区分哪些需要移动,哪些不需要移动呢? * 通过key的hash值直接对old capacity进行按位与&操作如果结果等于0,那么不需要移动反之需要进行移动并且移动的位置等于old capacity + 当前index。 * * hash & old capacity(8) * n : 1000 & index * 15 : 1111 = 1000 * 23 : 10111 = 0000 * 31 : 11111 = 1000 * 39 : 100111 = 0000 * 47 : 101111 = 1000 * 55 : 110111 = 0000 * 63 : 111111 = 1000 * * 从下面截图可以看到通过源码中的处理方式可以拿到两个链表,需要移动的链表15->31->47->63,不需要移动的链表23->39->55; * 因此扩容的时候只需要把loHead放到原来的下标索引j(本例j=7),hiHead放到oldCap + j(本例为8 + 7 = 15) * * @param args */ public static void main(String[] args) { HashMap<Integer, Integer> map = new HashMap<>(8); for (int i = 1; i <= 7; i++) { int sevenSlot = i * 8 + 7; map.put(sevenSlot, sevenSlot); } }
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不为空 && table长度大于0 && table索引位置(根据hash值计算出)不为空 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; // first的key等于传入的key则返回first对象 if ((e = first.next) != null) { // 向下遍历 if (first instanceof TreeNode) // 判断是否为TreeNode // 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 走到这代表节点为链表节点 do { // 向下遍历链表, 直至找到节点的key和传入的key相等时,返回该节点 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); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table是否为空或者length等于0, 如果是则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个 if ((p = tab[i = (n - 1) & hash]) == null)// 将索引位置的头节点赋值给p tab[i] = newNode(hash, key, value, null); else { // table表该索引位置不为空 Node<K,V> e; K k; if (p.hash == hash && // 判断p节点的hash值和key值是否跟传入的hash值和key值相等 ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果相等, 则p节点即为要查找的目标节点,赋值给e // 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 走到这代表p节点为普通链表节点 for (int binCount = 0; ; ++binCount) { // 遍历此链表, binCount用于统计节点数 if ((e = p.next) == null) { // p.next为空代表不存在目标节点则新增一个节点插入链表尾部 p.next = newNode(hash, key, value, null); // 计算节点是否超过8个, 减一是因为循环是从p节点的下一个节点开始的 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash);// 如果超过8个,调用treeifyBin方法将该链表转换为红黑树 break; } if (e.hash == hash && // e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环 ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 将p指向下一个节点 } } // e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用于LinkedHashMap return oldValue; } } ++modCount; if (++size > threshold) // 插入节点后超过阈值则进行扩容 resize(); afterNodeInsertion(evict); // 用于LinkedHashMap return null; }
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p 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; // 如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node 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) // 如果p是TreeNode则调用红黑树的方法查找节点 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { // 遍历链表查找符合条件的节点 // 当节点的hash值和key与传入的相同,则该节点即为目标节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; // 赋值给node, 并跳出循环 break; } p = e; // p节点赋值为本次结束的e } while ((e = e.next) != null); // 指向像一个节点 } } // 如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) // 如果是TreeNode则调用红黑树的移除方法 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 走到这代表节点是普通链表节点 // 如果node是该索引位置的头结点则直接将该索引位置的值赋值为node的next节点 else if (node == p) tab[index] = node.next; // 否则将node的上一个节点的next属性设置为node的next节点, // 即将node节点移除, 将node的上下节点进行关联(链表的移除) else p.next = node.next; ++modCount; // 修改次数+1 --size; // table的总节点数-1 afterNodeRemoval(node); // 供LinkedHashMap使用 return node; // 返回被移除的节点 } } return null; }
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { // 链表的处理start // ...代码省略... // 如果root的父节点不为空, 则将root赋值为根结点 // (root在上面被赋值为索引位置的头结点, 索引位置的头节点并不一定为红黑树的根结点) if (root.parent != null) root = root.root(); // 通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回 // (转链表后就无需再进行下面的红黑树处理) if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { tab[index] = first.untreeify(map); // too small return; } // 链表的处理end // 以下代码为红黑树的处理, 上面的代码已经将链表的部分处理完成 // 上面已经说了this为要被移除的node节点, // 将p赋值为node节点,pl赋值为node的左节点,pr赋值为node的右节点 // ...代码省略... }
- 红黑树不是节点数量小于8就立马又变成链表的,这个应该很好理解,等于是有个缓冲区! - 如果node不是TreeNode,那么就用链表的删除方法即可,这个是很简单的,直接改指针的指向就行。 - 最后结束流程,返回被删除的节点node的value值。