目录
- 一、知识点
- 二、数据结构
- 三、resize() 扩容方法
- 四、putVal() 添加数据方法
- 五、remove() 删除方法
- 六、removeTreeNode() 退化链表方法
一、知识点
加载因子:
HashMap 的默认的加载因子: 0.75,用来限定阈值(用于控制 HashMap 的饱和度)
阈值 = 加载因子 * 数组长度
默认阈值: 16 * 0.75 = 12 (默认的初始容量:16)
在数组中存储元素数量,超过阈值,会引发数组扩容
Hash算法:
不是加密算法,不可逆
常见的哈希算法:MD4 、 MD5 、SHA1、SHA2、 SHA3
hashmap 的 hash值的计算:
// 不是哈希算法,仿照哈希算法思想
// 不够充分随机,但是速度快
// 要存储 key-value 数据,对 key 计算,得到一个 int 类型的 hash 值
// 如果kay值是null, 它对应的hash值是0
// 如果kay值不是null, 取key的hashCode 异或上 hashCode向right移动16位
// hashCode 向低位移动16位, 让高位和低位取异或运算的目的, 就是希望高位和低位充分混合, 最终都能参加到取模预算中去
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);手动计算HashMap的hash值举例:
String str = "study";
int hash = str.hashCode();
hash = hash ^ (hash >>> 16);
// 将哈希值映射到有限的索引范围内
int index = hash % 底层数组长度;// hashCode(用于记录对象的位置)不一样, 那么hash值一定不一样, hashCode和hash值是一一对应关系
如果一个key-value数据在数组x下标位置, 当这个数组扩容, 扩容完成之后, 这个key-value数据要重新散列, 要么还在x位置, 要么在旧数组长度+x位置
因为默认的扩容机制是2倍
二、数据结构
class HashMap{Node[] table; // HashMap的底层数组int size;float loadFactor; // 加载因子float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子int threshold; // 阈值// ......
}
// 存储到HashMap底层Node数组中结点的类型:
// 添加key-value数据,实际上存储到对应经过计算 hash 值取模得到的下标的位置
class Node<K,V> {final int hash; // 扩容数组后,原本存在某个位置的元素可能改变位置final K key;V value;Node<K,V> next; // HashMap底层数组中是链表
}
三、resize() 扩容方法
给定数组长度点和 ArrayDeque 中的 initialCapacity 类似,只是 initialCapacity 是大于,没有等于
在构造方法里给 HashMap 一个长度, 它会把这个长度变成一个大于等于这个值的最小的2的幂值作为底层数组的长度
HashMap 的底层结构一直会保持2的幂值
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; // 默认的扩容机制:2倍}else if (oldThr > 0) // 场景一:第一次添加数据扩容,阙值不为0,数组为0// 比如调用构造方法 new HashMap(initCap,loadFactor); new HashMap(initCap); 等newCap = oldThr;else { // 场景一:第一次添加数据扩容,oldTab = null,oldCap = 0,oldThr = 0 // DEFAULT_INITIAL_CAPACITY = 1 << 4;// newCap(新容量) = 16newCap = DEFAULT_INITIAL_CAPACITY;// newThr(新阈值) = 0.75 * 16 = 12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 新阙值未赋值,如上面的if-else if不成立if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 上面的代码是在创建新的数组// 这里把旧数组数据转移到新数组中if (oldTab != null) {...}return newTab;
}
四、putVal() 添加数据方法
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;// 扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// (n - 1) & hash : hash值和数组长度取模// 因为数组长度是2的幂值,所以可以用&操作// i 取模之后的下标// p 指向key-value数据经过取模之后的下标位置if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K, V> e; // e指向需要修改的元素K k;// 要添加的key的位置,不为空,比较key// 先比较hash值,再看两个key是否直接相等或者相equalsif (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 {// 这个下标位置存储的不是树,是链表// 初始:p指向数组的“第一个元素”for (int binCount = 0; ; ++binCount) {// 注意:这里 e = p.next,e指向链表的第二个元素(数组中的数组为第一个元素),此时 binCount == 0if (( e = p.next) == null) {p.next = newNode(hash, key, value, null); // 当链表长度超过8达到9个数据的时候会进行红黑树的转化(阈值是8)// HashMap中出现红黑树的概率很低if (binCount >= TREEIFY_THRESHOLD - 1) // >= 7// 转化树,不一定会构造树(发生树的转化,但不一定会将链表转换成红黑树)treeifyBin(tab, hash);break;}// 链表中有重复元素, key相同,新值覆盖旧值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // 新值覆盖旧值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// ...return null;
}
转化树的源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;
当底层数组数为空,或者底层数组的长度 < MIN_TREEIFY_CAPACITY (初始值为64) 时,会选择扩容,而不是创建树
因为底层数组的长度是2的幂次值,< 64,即底层数组的长度最大为 32,0.75 的加载因子,即底层数组允许存储的数据的最大值是 32*0.75 = 24,这时即使某一个下标位置已经产生长度为9的链表, 是选择扩容, 而非转化为红黑树resize();if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)注意:
如果我们使用某些特殊的类型来充当key, 要根据上面这个条件判断是否需要重写hashCode和equals方法
五、remove() 删除方法
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;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 {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}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);else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;
}// 注:也可使用map.clear();将map中的所有元素全部删光
六、removeTreeNode() 退化链表方法
情况一:
移除元素 remove() 操作:
当红黑树(根节点/根节点的左右结点/根节点的左结点的左结点)这四个结点有任意一个不存在, 就要由红黑树转化为链表
在final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean movable) {...}中
关键代码:
if (root == null || root.right == null ||(rl = root.left) == null || rl.left == null) {tab[index] = first.untreeify(map); // too smallreturn;
}情形二:
底层数组的扩容resize()方法,导致树拆成两部分,树中的结点过少,转化成链表
判断节点类型,如果是链表,则将链表拆分,如果是TreeNode,则执行TreeNode的split方法分割红黑树
if (lc <= UNTREEIFY_THRESHOLD) // 低位;UNTREEIFY_THRESHOLD = 6tab[index] = loHead.untreeify(map);
if (hc <= UNTREEIFY_THRESHOLD) // 高位tab[index + bit] = hiHead.untreeify(map);
为什么链表转化为红黑树阈值是8, 红黑树转化为链表阈值是6 ?
担心如果阈值设为一样, 添加和删除可能导致红黑树和链表间的反复转化