目录
HashMap的结构
数组+链表或红黑树
链表节点数大于8时转为红黑树,小于6时退回链表
HashMap的继承关系
HashMap整体继承关系
从上图可见,HashMap继承了AbstractMap<K,V>类,实现了Map<K,V>类, Cloneable类, Serializable类
存放key-value的Node<key,value>节点和TreeNode<K,V>节点的继承关系
- Node<key,value>节点的结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //Hash值
final K key; //key值
V value; // value值
Node<K,V> next; // 下一个节点的地址值
}
- 红黑树里的Node<key,value>节点
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; // 前驱节点
boolean red;
}
- 节点的具体继承关系如下:
上图可知, 树节点TreeNode<K,V>继承了LinkedHashMap里的Entry<K,V>即LinkedHashMap.Entry<K,V>,而LinkedHashMap.Entry<K,V>又继承了HashMap.Node<K,V>,HashMap.Node<K,V>则实现了Map.Entry<K,V>节点
学习HashMap必须看懂的一行代码
hash & tab.length-1
这行代码在HashMap中出现过很多次,表面上看是用来确定HashMap中table的数组下标。实际上只是做了一个快速取余的操作,下面我们来仔细看看这行代码。
我们知道HashMap中规定table的长度一定是2的幂,其二进制表示为100000000······;而tab.length-1则为2^n-1,其二进制表示为011111111······;&为与运算符;hash & tab.length-1代表的是hash值与tab.length-1按位与。这样最后得到的结合怎样都不会大于tab.length,即一定不会发生数组越界。
HashMap的部分核心参数
HashMap中的部分参数
- Node<K,V>[] table:存放Node的数组
- int size:表示当前HashMap包含的键值对数量
- int modCount:用于记录
- int threshold:表示当前HashMap能够承受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩容
- final float loadFactor:哈希表的负载因子,用于扩容
HashMap中的静态变量
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
默认初始容量 - 必须是 2 的幂。 - static final int MAXIMUM_CAPACITY = 1 << 30
最大容量,在两个带参数的构造函数隐式指定更高值时使用。 必须是 2 的幂 <= 1<<30。 - static final float DEFAULT_LOAD_FACTOR = 0.75f
在构造函数中未指定时使用的负载因子。 - static final int TREEIFY_THRESHOLD = 8
判断链表是否转化为红黑树的阈值,值为8 - static final int UNTREEIFY_THRESHOLD = 6
判断是否应该由红黑树退回链表的阈值,值为6
HashMap之初始化构造方法
- 构造一个具有默认初始容量 (16) 和默认负载因子 (0.75) 的空HashMap
public HashMap() {
// 所有的属性均为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
- 构造一个具有指定初始容量和默认负载因子 (0.75) 的空HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 构造一个具有指定初始容量和负载因子的空HashMap
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);
}
- 使用与指定Map相同的映射构造一个新的HashMap 。 HashMap是使用默认负载因子 (0.75) 创建的,初始容量足以在指定的Map 中保存映射。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HasMap之public V put(K key, V value)方法
方法传入的参数为:
- 待put进HasMap的<Key,Value>键值对
具体过程分析:
- 数组为空,数组还未初始化:调用resize()初始化数组,直接放入table相应位置。(resize扩容方法很重要)
- table中该位置没产生Hash冲突:构造节点放入table中
- 产生Hash冲突,先判断table中节点元素key是否相等,相等则替换value
- 产生Hash冲突,节点是TreeNode:按红黑树的方式插入节点
- 产生Hash冲突,节点是Node:构造节点按链表的方式插入,并且检查插入后是否到达转红黑树的阈值8
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;
// 数组为空,第一次put 调用resize()进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 查询数组中的对应位置是否产生hash冲突
if ((p = tab[i = (n - 1) & hash]) == null)
// 构造Node节点放入table中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判断table中元素的key的hash值是否与待put元素ke的hash值相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 使用e保存p的节点引用,后续替换节点引用
e = p;
// 判断table中的p节点是否为树节点
else if (p instanceof TreeNode)
// p是树节点,使用红黑树的插入方式插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// p是链表节点,采用链表的插入方式插入
// for循环遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 插入节点
p.next = newNode(hash, key, value, null);
// 判断当前链表节点个数是否大于等阈值8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 当前节点个数大于等于8,将当前节点插入并树化链表
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 不是需要插入新节点的情况
// 则找到key相同的节点替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 判断是否需要扩容
// 即存在的节点数size>threshold(容量*负载因子)时,就需要扩容
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put()方法中的treeifyBin(Node K,V [] tab, int hash)
方法传入的参数为:
- Node<K,V>[] tab:存放 Node<K,V>节点的数组
- int hash:当前put的key的hash值
具体过程分析:
- 判断tab数组是否为空或者长度小于最小树化大小MIN_TREEIFY_CAPACITY(64)
a.如果判断条件成立,则不会树化链表而是调用resize()方法进行扩容。扩容之后链表长度会变小,提高查询效率。
b.如果判断条件不成立,则树化链表。 - 树化链表:
a.遍历链表,并将链表节点的属性赋值给新构造的TreeNode节
b.遍历结束后,将hd的值赋给table[index],并判断是否不为空,不为空则调用treeify()方法完成树化。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 判断数组是否为空,或者数组长度<64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 情况成立,则不会树化链表而是调用resize()方法进行扩容
// 扩容之后链表长度会变小,提高查询效率
resize();
// 判断输入的hash值在table中对应的位置是否不为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 不为空,树化
TreeNode<K,V> hd = null, tl = null;
do {
// 遍历链表,并将链表节点的属性赋值给新构造的TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 将hd的值赋给table[index],并判断是否不为空
if ((tab[index] = hd) != null)
// 不为空,完成树化
hd.treeify(tab);
}
}
HasMap之public V get(Object key)方法
方法传入参数为:
- Object key用于获取键值对value值的key
具体过程分析:
- 判断元素是否在数组中
- 不在则返回null
- 在则判断数组中的元素是否与参数key相等
- 相等则返回数组中的元素
- 不相等则判断该元素是否存在next元素,若存在,说明存在链表或者树。然后判断该元素是否为树节点
- 是树节点则按照遍历树的方式找到目标节点
- 若不是,则按照遍历链表的方式找到节点
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;
// 判断数组是否不为空,数组长度是否大于0,Key对应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;
if ((e = first.next) != null) {
// 判断是否是树节点
if (first instanceof TreeNode)
// 红黑树,调用getTreeNode()方法(和二叉查找树逻辑差不多)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 链表情况
do {
// 遍历判断hash值是否相等,相等则返回节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HasMap之public V remove(Object key)方法
方法传入参数为:
- Object key用于移除HashMap对应键值对的key
具体过程分析:
- 判断数组是否不为空以及判断key的hash值确定的位置是否不存在元素
a.若判断条件成立。
1.判断节点是否在数组上。若在数组上则将当前hash值确定的节点 赋值给node;节点不在数组上。则判断是否在红黑树上,若在则调用getTreeNode方法获取该节点并赋值给node。若不在即节点在链表上,则遍历链表获取该节点赋值给node并结束遍历。
2.判断node是否为树节点。若为树节点,则调用树节点的remove方法;若不为树节点(即为链表节点),则将节点后继赋值给数组中的元素,并将节点后继赋值给节点前驱的后继。
b.若其中之一不成立。即key不在hashmap中,直接返回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;
// 判断数组是否不为空以及判断key的hash值确定的位置是否存在元素,若其中之一不成立,即key不在hashmap中,直接返回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)
// 节点在红黑树上,调用getTreeNode方法
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 节点在链表上
else {
// 遍历链表,寻找与节点的hash值或者节点key相等的节点
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)
// 调用树节点的remove方法
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 将节点后继赋值给数组中的元素
tab[index] = node.next;
else
// 将节点后继赋值给节点前驱的后继
p.next = node.next;
++modCount;
--size;
// 预留方法,当类继承HashMap后可以重写该方法
// 使得在节点移除后完成指定操作
afterNodeRemoval(node);
return node;
}
}
return null;
}
HasMap之final Node K,V [] resize()方法
什么使用调用resize()方法:
- 第一次初始化时候调用
- 存放的键值对的数量>hashmap容量*负载因子就需要扩容
具体过程分析:
直接分析扩容的的相关代码 - 如果当前数组不为空,则容量和阈值都扩大成两倍,并更新threshold属性
- 新建一个大小是当前数组大小两倍的数组,并更新table属性
- 迁移之前的节点到相应位置
a.遍历当前数组的每一个元素,并判断元素是否不为空。不为空,则把当前元素赋值给e并置空当前元素。
b.判断e.next 是否为空。若为空,则重新对新的容量快速取余,并将e存到新数组的相应位置;不为空,则判断是否为树节点。若为树节点则调用split对红黑树进行扩容;不为树节点则为链表节点。
c.链表情况。遍历链表元素将一个链表拆成两个链表。拆分依据为:扩容前后节点是否需要移动,即进行快速取余操作之后,余数有无变化
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 判断当前数组大小oldCap 是否大于0
if (oldCap > 0) {
// 判断oldCap 是否大于等于当前数组的最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 左移1位,即oldThr*2
newThr = oldThr << 1;
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 初始化table
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;
// 判断每一个元素是否不为null,并将元素值和属性赋值给e
if ((e = oldTab[j]) != null) {
// 将该元素置空
oldTab[j] = null;
// p判断e.next是否为空
if (e.next == null)
// 为空则重新对新的容量快速取余,并将e存到新数组的相应位置
newTab[e.hash & (newCap - 1)] = e;
// 不为空,则判断是否是一个树节点
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;
// e.hash & oldCap结果等于0的节点存放在loTail链表下
//扩容后余数与之前一致,不需要移动的节点。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// e.hash & oldCap结果不等于0的节点存放在hiTail链表下
//扩容后余数与之前不一致,需要移动的节点。
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null)
//扩容后余数与之前不一致,需要移动的节点。
if (loTail != null) {
loTail.next = null;
//loTail链表的头结点赋值给新数组的当前位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// hiTail链表的头结点赋值给新数组与当前位置相隔扩容前数组大小的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize()方法中的final void split(HashMap K,V map, Node K,V [] tab, int index, int bit)
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 遍历红黑树,将红黑树拆分成两个链表
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
// 如果loTail节点的个数小于6
if (lc <= UNTREEIFY_THRESHOLD)
// 红黑树转化为链表,并将loHead赋值给数组的index位置
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
// bit原数组大小
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}