存储结构
数组+链表+红黑树,1.8版本当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会转为红黑树
几个属性
// 默认容量16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表节点转换红黑树节点的阈值, 9个节点转static final int TREEIFY_THRESHOLD = 8;// 红黑树节点转换链表节点的阈值, 6个节点转static final int UNTREEIFY_THRESHOLD = 6;// 转红黑树时, table的最小长度static final int MIN_TREEIFY_CAPACITY = 64;// 链表节点, 继承自Entrystatic class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;// ... ...}// 红黑树节点static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red;// ...}
初始化
无参方法
使用默认负载因子 0.75
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;this.threshold = tableSizeFor(initialCapacity);}
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;}
这个算法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数),该算法让最高位的1后面的位全变为1,详情可参考 HashMap中tableSizeFor
put(K key, V value)
先整体看下代码
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;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
先看下hash值的计算
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
16 说是为了增加散列度,在 table 的 length 较小的时候,让高位也参与运算,可参考 HashMap计算hashCode时为什么要把高位右移16位
put 的步骤
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],插入节点到末位,判断链表长度是否大于8,大于8的话把链表转换为红黑树;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
计算数组下标
resize() 稍后再看,先看下数组下标计算方法
i = (n - 1) & hash
n 是数组长度,使用hash值取模,这个方法非常巧妙,基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率,为什么 mod 2^n = x & (2^n - 1) 可参见 计算一个数与2的n次方取模
存放数据
计算table下标后,如果table[i] == null,直接新建节点,主要看下非空的情况
key 相同 & equal 比较相等的情况下,直接替换
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){e = p;}
判断是否是 TreeNode,即是否是红黑树,如果是红黑树,则直接在树中插入键值对,执行 putTreeVal 方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
遍历table[i],插入节点到末位,判断链表长度是否大于8,大于8的话把链表转换为红黑树;遍历过程中若发现key已经存在直接覆盖value即可 ```java for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
<a name="treeifyBin"></a>#### treeifyBin
/**
- 将链表节点转为红黑树节点
*/
final void treeifyBin(Node
[] tab, int hash) { int n, index; Node e; // 1.如果table为空或者table的长度小于64, 调用resize方法进行扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 2.根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表 else if ((e = tab[index = (n - 1) & hash]) != null) {resize();
} } ```TreeNode<K,V> hd = null, tl = null;do {// 3.将链表节点转红黑树节点TreeNode<K,V> p = replacementTreeNode(e, null);// 4.如果是第一次遍历,将头节点赋值给hdif (tl == null) // tl为空代表为第一次循环hd = p;else {// 5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性p.prev = tl; // 当前节点的prev属性设为上一个节点tl.next = p; // 上一个节点的next属性设置为当前节点}// 6.将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)tl = p;} while ((e = e.next) != null);// 7.将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树if ((tab[index] = hd) != null)hd.treeify(tab);
resize
具体代码参见文末引用
为什么扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?
正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。
hash&(Table.length-1) 就是hash对table长度取模,有两种情况:
- hash值是旧table长度的偶数倍(2n)倍 + 取模值,那么table扩容后hash值就是 n 倍table新长度 + 取模值了,还是原来位置
- hash 值是旧table长度的奇数(2n + 1)倍 + 取模值,那么table扩容后hash值就是 n 倍table新长度+ 旧table长度 + 取模值
get(Object key)
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;// 1.对table进行校验:table不为空 && table长度大于0 &&// table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 3.如果first不是目标节点,并且first的next节点不为空则继续遍历if ((e = first.next) != null) {if (first instanceof TreeNode)// 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNodereturn ((TreeNode<K,V>)first).getTreeNode(hash, key);do {// 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 6.找不到符合的返回空return null;
remove(Object key)
/*** 移除某个节点*/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;// 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给pif ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;// 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给nodeif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {// 3.否则将p.next赋值给e,向下遍历节点// 3.1 如果p是TreeNode则调用红黑树的方法查找节点if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {// 3.2 否则,进行普通链表节点的查找do {// 当节点的hash值和key与传入的相同,则该节点即为目标节点if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e; // 赋值给node, 并跳出循环break;}p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点} while ((e = e.next) != null); // e指向下一个节点}}// 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {// 4.1 如果是TreeNode则调用红黑树的移除方法if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);// 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点else if (node == p)tab[index] = node.next;// 4.3 否则将node的上一个节点的next属性设置为node的next节点,// 即将node节点移除, 将node的上下节点进行关联(链表的移除)elsep.next = node.next;++modCount;--size;afterNodeRemoval(node); // 供LinkedHashMap使用// 5.返回被移除的节点return node;}}return null;}
死循环问题
HashMap 和 Hashtable 的区别
- HashMap 允许 key 和 value 为 null,Hashtable 不允许。
- HashMap 的默认初始容量为 16,Hashtable 为 11。
- HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
- HashMap 是非线程安全的,Hashtable是线程安全的。
- HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
- HashMap 去掉了 Hashtable 中的 contains 方法。
- HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。
