0.底层实现加了红黑树
数组+链表(链表长度超过阈值,默认8-->红黑树)
HashMap在进行put get remove的时候,都是先计算hash,然后根据hash定位桶的位置(table[]的下标),桶中是hash值冲突的键值对组成的链表,然后遍历该链表进行相应的操作,当冲突很多时,遍历链表效率低,红黑树的效率高
1.用于描述键值对的内部类由Entry<K,V>变成了Node<K,V>,多了一个描述红黑树的内部类TreeNode<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //1.7中,hash不是final的
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 links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
……
}
2.成员变量
transient Node<K,V>[] table; 哈希表
static final int TREEIFY_THRESHOLD = 8; 一个桶中冲突的个数大于该值时,将链表转化成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
static final int UNTREEIFY_THRESHOLD = 6; 一个桶中冲突的个数小于该值时,将红黑树转化成链表
static final int MIN_TREEIFY_CAPACITY = 64; 该值最少为TREEIFY_THRESHOLD 的4倍,将一个链表转化成二叉树的过程中,如果table.length小于该值,会resize(),,也就是说,当桶的个数(Node数组的长度)小于该值的情况下,一个桶中的链表的长度大于TREEIFY_THRESHOLD 阈值,需要进行链表转换成红黑树,此时,进行扩容,而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
3.构造器
在构造器中判断参数合法之后,初始化loadFactor(加载因子)和threShold(阈值),并不真正初始化哈希表,即存储键值对的数据结构并不是在构造方法里初始化的,而是在第一次调用put方法时,在put方法中调用resize方法进行哈希表的初始化
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); //该方法将返回一个大于等于initialCapacity的2的幂次方的值 }
1.7中,将阈值初始化为初始容量 threshold = initialCapacity; 在之后,第一次put的时候,需要调用inflateTable方法初始化哈希表,在此时,会将容量调整为大于等于初始容量的2的幂次方的值
1.8中,阈值初始化为大于等于初始容量的2的幂次方的值 this.threshold = tableSizeFor(initialCapacity);
何时初始化存储键值对的数据结构?
1.7 在调用put方法时,如果table是null,则初始化Entry<K,V>[] table,调用initfatetable方法初始化,在该方法中,将容量调整为大于等于initialCapacity的2的幂次方的值。
1.8 在调用put方法时,如果table是null,则初始化Node<K,V>[] table,调用resize方法初始化
4.hash计算
hash值的作用:定位桶的位置(n - 1) & hash n = table.length,定位桶的位置要用哈希表的长度-1 & hash值
为何hash没有直接使用对象的HashCode?
哈希表的长度n相较于对象的HashCode比较小,如果直接使用对象的HashCode和(n-1)作与运算,可能HashCode只有低几位参与了运算,因此,hash值是对对象的HashCode调整之后的值,为的是让HashCode的高几位也参与运算,减少随机性带来的影响。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.7和1.8计算hash值的方法不一样,但是都是对对象的HashCode做了一些位运算,目的都是为了减少哈希冲突
5.查找get
定位桶的位置,看桶中是TreeNode类型的红黑树,还是Node类型的链表
调用查找红黑树的方法 or 遍历链表
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;
// 如果有key对应的键值对,返回其value,没有,返回null
// 所以,返回null时,有可能键对应的值为null,有可能不存在相应的键值对
} final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果哈希表是Null,表明再创建HashMap之后,还没有put元素,直接返回 null
哈希表不是null,定位桶的位置 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 先判断第一个结点
如果第一个结点就是要找的键值对,直接返回
如果第一个结点不是要找的键值对,那么, 判断桶中是红黑树还是链表
红黑树:调用TreeNode的getTreeNode方法查找
链表:遍历链表查找
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) 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;
6.插入put,如果键值有重复的,返回旧值;没有重复的,返回null
扩容、树化(将桶中的链表转化成红黑树)
调用put方法, 计算hash(key),计算hash值是为了定位桶的位置
在put方法中调用putVal方法
如果哈希表是null(调用构造器创建HashMap之后还没有进行table的初始化),调用resize方法初始化哈希表
如果已经初始化了,定位桶的位置 length-1 & hash
如果当前桶是空的,直接创建Node并插入
如果当前桶不是空的(有冲突)
判断第一个结点(不论put、get、remove都先判断第一个结点),看当前桶中第一个结点是否和要插入的键值对的键重复,重复,用新值覆盖旧值,结束
不重复,判断当前桶中是链表还是红黑树
红黑树:调用内部类TreeNode的putTreeVal方法,如果红黑树中有重复的,将重复的节点返回,之后用新值覆盖旧值;没有重复的,插入在合适位置
链表:从头到尾遍历链表,没有重复的,插在链表尾部;有重复的,将重复的节点记录下来,之后用新值覆盖旧值
注意:用新值覆盖旧值是有条件的,putValue的方法入参onlyIfAbsent决定了是否仅在旧的value值==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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //调用构造器创建HashMap之后,只是初始化了加载因子和阈值,并为初始化Node<K,V>[] table,所以,在第一次put时,先初始化table哈希表
//初始化哈希表
1.7 调用inflateTable方法初始化哈希表transient Entry<K,V>[] table,将指定初始容量值调整为大于等于指定初始容量的2的幂次方的值之后,table = new Entry[capacity]
1.8 调用resize方法初始化哈希表transient Node<K,V>[] table
if ((p = tab[i = (n - 1) & hash]) == null) //定位桶的位置,如果当前桶是空的,表明不冲突,直接新建Node并插入 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中,后面用新值覆盖旧值
否则,判断该桶中是链表还是红黑树,分别进行处理
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) {
// 遍历到链表尾部了,证明一直没有一样的键,所以,新建Node插入在链表尾部 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果当前链表的长度超过了阈值,将链表转化成红黑树 treeifyBin(tab, hash); break; }
// 在遍历链表的过程中,如果发现有一样的键,和上面一样,将该节点Node记录在e中,后面用新值覆盖旧值 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
// 此处,如果e != null,证明上面有和要插入的键值对一样的键,用新值覆盖旧值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;
// 如果哈希表的总长度超过了阈值,扩容。 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
7.扩容
扩大的是Node<K,V>[] table的长度,扩充为原来的2倍,并且要重新计算键值对的位置,将其放在何时的桶中,扩容之后,桶的数量增加,但是冲突减少
table数组的长度总是2的幂,原因:用位运算代替取余运算来定位桶的位置
何时扩容? HashMap中元素的个数 大于 阈值(容量*加载因子)
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //记录扩容之前的table的状态 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //之前的容量大于0,表明已经初始化了,此次调用resize方法是为了扩大哈希表的容量 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; //已经没有可扩充的余地了,将阈值设为MAX_VALUE,直接返回原哈希表 } 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
//oldCap == 0 表示还没初始化, 但是oldThr已经在构造器中设为 指定容量对应的2的幂次方的值
//那么初始化之后的容量即为oldThr中保存的值 newCap = oldThr; else { // zero initial threshold signifies using defaults // oldCap == 0 并且 oldThr == 0 表明没有初始化并且调用构造器时没有指定容量,将它们都设为默认值
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; // 创建哈希表并赋给成员变量table
// 将旧table中的键值对一一计算其hash值,放在新的哈希表的合适位置; 如果是初始化,oldTab是null,下面的操作不用进行 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) //如果桶中只有一个Node,那就直接计算其新的数组下标 newTab[e.hash & (newCap - 1)] = e;
// 如果桶中有多个Node,如果是红黑树,要对红黑树进行查分;如果是链表,遍历链表 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order 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; }
8.树化
9.remove
根据一个key删除对应的键值对,返回键值对对应的value
先遍历HashMap,找到key对应的节点并用变量node记录下来
如果node是null,表明没有对应的节点,直接返回null
如果node不是nul,表明存在要删除的节点
如果node是红黑树的一个节点,调用removeTreeNode方法删除
如果node是链表的第一个结点,tab[index] = node.next;
不是链表的第一个结点,p.next = node.next;
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
// 删除key对应的Node,返回对应的value值或者null } 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是null,直接返回 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; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }