HashMap
java中hashmap是一个二维的数据结构,可以从x,y轴来理解
x轴向是一个数组
y轴向是一个链表/红黑树
兼具了数组和红黑树的优点
同时拓展了数组的优点
数组是索引和元素分离 2者没有关系 hashmap的索引是通过元素生成的,建立了索引和元素之间的一种关系,更加利于查找。
同时纵向的链表和红黑树 提高了插入和删除的效率。
但是hashmap的缺点是天然无序,数组插入可以实现有序,但是map却不能实现有序。索引的生成方式决定了他的无序性
删除
参数
-
key 必须
-
value 可选
流程
-
数组为null 数组长度为0 或者 索引处的值为null 说明没有命中 返回null
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { //链表存在 } return null;
-
如果索引命中,就从链表中寻找
-
如果hash相等 并且 key相等 说明头节点就是要删除的节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
这里要注意 hashmap 允许null作为key 也同样支持删除key为null的entry
同时要注意 这里只是找到了key 后面还要判断value是否能匹配到
-
如果不是头节点 要对链表进行循环查找
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)))) { }
这里要注意 是否同时找到了key和value
node !=null 说明找到了 key
matchValue==value 说明找到了value
-
如果找到了就要进行删除操作
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;
注意 红黑树和链表的删除方式不一样
注意 **p是node的前向节点 ** node==p 说明要删除的是头节点 要修改头节点
-
删除后续操作
//修改次数加1 ++modCount; //元素个数减1 --size; //可以在子类拓展的方法 默认是个空函数 afterNodeRemoval(node); return node;
注意 java不需要手动释放内存
-
红黑树的查找
-
红黑树的删除
-
复杂度
清空
这里很简单 没有采用每个节点都删除的方式
只是简单的把数组的每个元素都置为null 把size设置为0而已
插入
代码
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);
//如果有冲突
//如果到这步的情况多,不能很好体现hashmap的优点
else {
Node<K,V> e; K k;
//如果hash和key都相等 说明键已经存在了 并且等于头节点
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 1st
treeifyBin(tab, hash);
break;
}
//如果key相等 说明已经存在映射 跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//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;
}
}
++modCount;
//判断是否是否超出数组阈值 如果超出 要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
注意 hash冲突和key冲突 是两个不同的概念
注意 hash冲突时 要向链表或是红黑中插入
注意 key冲突时 要根据 onlyIfAbsent 值和 null 值 判断是否覆盖旧值
注意 链表转红黑树的阈值默认是 8
注意 **数组的阈值默认是 数组的长度 * 负载因子 **
参数
-
key 必须
-
value 必须
流程
初始化
阈值设置为 大于 2(n-1)<x<2n
大于x的第一个2的幂次
比如说 HashMap(13) threashold=16
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
扩容
代码
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; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//newCap又等于旧的阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 阈值是 newCap*负载因子
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//数组的容量是 newCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果不是第一次扩容
//也就是说数组里面已经有值 那还得进行元素的重新hash,重新填充
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)
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定义了几个拓展函数(钩子函数),方便子类拓展
LinkedHashMap就实现了这几个方法
//修改后
void afterNodeAccess(Node<K,V> p) { }
//插入后
void afterNodeInsertion(boolean evict) { }
//删除后
void afterNodeRemoval(Node<K,V> p) { }
问题
hashmap 数组扩容是根据size和阈值来判断的
阈值是数组大小和负载因子的乘积 和 数组大小相关
但是size和数组的填充量并没有直接的关系,但是存在确是很合理的
比如说 这种情况
数组大小为16 阈值有12 有12个元素的hash值都是1样的的,这样就相当于数组只用了一个槽,全部挂载到了这一个槽的链表/树上,这样就不能体现hashmap的优点了,所以需要重新hash来将元素重新划分到数组中去。