HashMap
文章目录
hashmap的实现原理:首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中
当table中的元素足够多时,发生冲突的概率就会大大增加,冲突的增多会导致每个桶中的元素个数变多,这样的话会使得查找元素效率变得低下,当同一个桶中元素个数达到8时,桶中的元素结构将转换为红黑树。
基本属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//序列号
private static final long serialVersionUID = 362498820763181265L;
//默认初始容量2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当桶(bucket)上的结点大于这个值就会转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当桶(bucket)上的结点小于这个值就会转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,总是2的幂次倍
transient Node<K,V>[] table;
//存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
//存放元素的有效个数
transient int size;
//记录修改次数
transient int modCount;
//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//填充因子
final float loadFactor;
构造函数
无参构造
public HashMap() {
//初始化填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
HashMap(int,float)型构造函数
需要对传入的初始容量和填充因子进行校验,初始容量转换为大于等于且最接近初始容量的2的幂次方整数
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不能大于最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//填充因子不能小于等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//验证是否为非数字,非数字就会返回true,
public static boolean isNaN(float v) {
return (v != v);
}
System.out.println(Float.isNaN(0.0f / 0.0f)); // true
System.out.println(Float.isNaN(0.0f)); // false
//返回一个大于等于且最接近 cap 的2的幂次方整数
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;
}
tableSizeFor()这个方法的作用是找到大于等于给定容量的最小2的次幂值
0000 0101------5
cap-1=4
0000 0100------4
右移1----0000 0010----或----
0000 0110
右移2----0000 0001----或----
0000 0111
右移4----0000 0000----或----
0000 0111-------7
…
n+1=8
-
减一:如果给定的cap已经是2的次幂,但是不进行-1操作的话,那么得到的值就是大于给定值的最小2的次幂值
-
右移16位就可以得到最大值32位1,再往后没有意义
HashMap(int)型构造函数
调用HashMap(int,float)型构造函数,填充因子为默认值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(Map)型构造函数
public HashMap(Map<? extends K, ? extends V> m) {
//初始化填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m中的所有元素添加至HashMap中
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
//先判断m中是否有值
if (s > 0) {
//判断table是否已经初始化
if (table == null) { // pre-size
//数组为空,初始化参数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
threshold = tableSizeFor(t);
}
//数组不为空,超过阈值就扩容
else if (s > threshold)
resize();
//将m中的所有元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//原容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原临界值
int oldThr = threshold;
int newCap, newThr = 0;
//原table不为空,不是第一次扩容
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; // double threshold
}
/**
* 从构造方法我们可以知道
* 如果没有指定初始化容量initialCapacity, 则不会给threshold赋值, 该值被初始化为0
* 如果指定了initialCapacity, 该值被初始化成大于initialCapacity的最小的2的次幂
*/
//这里这种情况指的是原table为空,并且在初始化的时候指定了容量,threshold被赋有值
else if (oldThr > 0)
// 则用threshold作为table的新容量,实际大小
newCap = oldThr;
//这里情况是原table为空,构造方法中没有制定容量,threshold=0
else {
//使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//计算指定了initialCapacity情况下的新的threshold临界值
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中
//分红黑树和链表讨论
if (oldTab != null) {
//根据容量循环,将非空元素赋值
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//获得数组的第j个元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果链表只有一个,直接进行复制
if (e.next == null)
//确定元素存放位置
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;
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;
}
HashMap常用方法
put方法
JDK8 HashMap put的基本思路:
1)对key的hashCode()进行hash后计算数组下标index;
2)如果当前数组table为null,进行resize()初始化;
3)如果没碰撞直接放到对应下标的位置上;
4)如果碰撞了,且节点已经存在,就替换掉 value;
5)如果碰撞后发现为树结构,挂载到树上。
6)如果碰撞后为链表,添加到链表尾,并判断链表如果过长(大于等于TREEIFY_THRESHOLD,默认8),就把链表转换成树结构;
7)数据 put 后,如果数据量超过threshold,就要resize。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash方法
从代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)
//hash处理
static final int hash(Object key) {
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
//高16位与低16位进行异或为第三步,异或0不变
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xoroshc-1576380702804)(images/hash.jpg)]
map中put、get操作时为什么不直接用原有的hash值?
这个与HashMap中table下标的计算有关。
因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。
假设table.length=2^4=16
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P6V9Wt1f-1576380702807)(images/index坐标.jpg)]
这样做很容易产生碰撞。设计者权衡了speed, utility, and quality,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
putVal方法
@param hash --key的哈希值
@param key --key
@param value --key将要映射的value
@param onlyIfAbsent --如果是true的话,将不会改变已存在的值
@param evict --这个参数如果为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未初始化,则先重新调整大小至初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n-1)& hash 这个地方即根据hash求序号,存放位置
if ((p = tab[i = (n - 1) & hash]) == null)
//不存在,没有碰撞冲突,则新建节点
tab[i] = newNode(hash, key, value, null);
else {
//节点存在值,发生冲突,存在节点p
Node<K,V> e; K k;
//先找到对应的node
// 若桶中第一个结点的hash值相同并且equals方法返回true时进行替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断是否为红黑树,如果是树节点,则调用相应的putVal方法
//todo putTreeVal
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);
//判断链表长度是否大于8
if (binCount >= TREEIFY_THRESHOLD - 1)
//如果链表长度达到树化的最大长度,则进行树化
//todo treeifyBin
treeifyBin(tab, hash);
break;
}
//匹配到p的下一个节点,key存在跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果已存在该key的映射,则将值进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//todo
afterNodeAccess(e);
return oldValue;
}
}
//修改次数加一
++modCount;
if (++size > threshold)
//todo
resize();
//todo
afterNodeInsertion(evict);
return null;
}
寻找key对应的映射关系:
e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
HashMap中通过hash & (length - 1)计算得到的索引位置,因此,不同的hash值可能会映射到同一个桶中,所以在寻找key对应的映射关系时,首先通过这个e.hash == hash条件过滤掉hash值不同的key,之后再通过((k = e.key) == key || (key != null && key.equals(k))),提高效率
treeifyBin方法
当桶中链表长度超过8时,会调用此方法将链表结点转红黑树结点进行如下操作:
1)判断是否真的需要转换红黑树,如果数组长度小于MIN_TREEIFY_CAPACITY
将会中扩容代替转换红黑树
2)如果符合转换的条件,先为链表转化红黑书做准备。将所有的节点转换成树形节点,并且构造成双链表。
final void treeifyBin(Node<K,V>[] tab, int hash) {
//n是数组长度,e是hash值和数组长度计算后,得到链表的首节点
int n, index; Node<K,V> e;
/** 如果数组为空或者数组长度小于树结构化的最小限制
* MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
* 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后得到的坐标结果相同。(并不是因为这些key的hash值相同)
* 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
*/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
/**Node对象转换成了TreeNode对象
*把单向链表转换成了双向链表
*/
// 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
// 根据hash值和数组长度进行取模运算后,得到链表的首节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;//hd是树首节点,tl是树尾节点
do {
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); // 继续遍历链表
// 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
// 把转换后的双向链表,替换原来位置上的单向链表
if ((tab[index] = hd) != null)// 若指定的位置头结点不为空则进行树形化
hd.treeify(tab);// 根据链表创建红黑树结构
}
}
// For treeifyBin
//节点转换成树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
treeify方法
链表转换成红黑树结构
红黑树转换步骤:
1)遍历链表中节点,桶中第一个结点作为红黑树根节点(黑色)
2)内层遍历树结点,从根结点开始通过比较哈希值寻找其位置
3)若x结点hash值小于p结点hash值,往p结点左边寻找,否则往p结点右边寻找
4)一直按照步骤③寻找,直至寻找的位置为null即此位置为x的目标位置
5)因为红黑树性质,其插入删除都需要平衡调整
6)最后确保红黑树根结点为桶中第一个节点
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;//定义树的根节点
/**
*外层对链表遍历,x指向当前节点
*/
for (TreeNode<K,V> x = this, next; x != null; x = next) {// 遍历链表,x指向当前节点、next指向下一个节点
next = (TreeNode<K,V>)x.next;//下一个节点
x.left = x.right = null;
if (root == null) {//还没有根节点,根节点指向当前节点
x.parent = null;
x.red = false;
root = x;
}
else {//根节点已经存在
K k = x.key;//取当前链表节点的key
int h = x.hash;//取当前链表节点的hash
Class<?> kc = null;// 定义key所属的Class
/**
*对树遍历,将当前结点x的hash与树上的树节点hash进行比较
*/
for (TreeNode<K,V> p = root;;) {// 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出,p是当前树节点
int dir, ph;// dir 标识方向(左右)、ph标识当前树节点的hash值
K pk = p.key;// 当前树节点的key
if ((ph = p.hash) > h)
dir = -1;// 标识当前链表节点会放到当前树节点的左侧
else if (ph < h)
dir = 1;//右侧
/*
* 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
* 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
* 如果还是相等,最后再通过tieBreakOrder比较一次
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;//保存当前红黑树的树节点
/*
* 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
* 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
* 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
* 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
* 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;//当前树节点 作为 当前链表节点的父节点
if (dir <= 0)
xp.left = x;//作为左孩子
else
xp.right = x;//右孩子
root = balanceInsertion(root, x);//重新平衡
break;
}
}
}
}
// 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
// 为了保证每次红黑树的根节点都在链表的第一个位置,在操作完成之后 需要moveRootToFront方法来进行调整
moveRootToFront(tab, root);
}
balanceInsertion:指的是红黑树的插入平衡算法,当树结构中新插入了一个节点后,要对树进行重新的结构化,以保证该树始终维持红黑树的特性。
关于红黑树的特性:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的路径上包含的黑色节点数量都相同。
moveRootToFront:当我们删除或者增加红黑树节点的时候,root节点在双链表中的位置可能会变动,为了保证每次红黑树的根节点都在链表的第一个位置,在操作完成之后 需要moveRootToFront方法来进行调整。
get方法
1)先匹配第一个节点,如果匹配到直接返回
2)如果有冲突,判断如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key;如果是链表,则采用遍历查找
/**
* 返回指定键映射的值,当该key不存在的时候返回null
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 实现了Map.get 和相关方法
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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)
//如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key
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;
}
resize方法
(k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树节点,则用TreeNode的getTreeNode方法来查找相应的key
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;
}
### resize方法
来源:CSDN
作者:baidu_35181420
链接:https://blog.csdn.net/baidu_35181420/article/details/103546990