hashMap分析
1. 默认的几个参数
初始容量大小
DEFAULT_INITIAL_CAPACITY = 1 << 4 (16)
hashMap最大容量
具体可以看__负载因子__里面的代码,最大的容量是1 << 30(1073741824)
负载因子
DEFAULT_LOAD_FACTOR = 0.75F
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
static final float DEFAULT_LOAD_FACTOR = 0.75f;/默认的加载因子是0.75
可以通过构造方法来改变负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//注意看这里,MAXIMUM_CAPACITY = 1 << 30(1073741824)
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//threshold必须是2的n次幂的整数数,这方法是返回 initialCapacity附近的2的n次幂的整数。
this.threshold = tableSizeFor(initialCapacity);
}
链表转为红黑数的条件
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//TREEIFY_THRESHOLD的值
static final int TREEIFY_THRESHOLD = 8;
//下面是treeifyBin的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
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);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
//MIN_TREEIFY_CAPACITY看看这个值
static final int MIN_TREEIFY_CAPACITY = 64;
链表的长度 >=
7并且数组的长度 >=64
2. 整体流程
整理的流程简单来说按照下面的流程
- 通过key计算hash值
- 计算key要放在数组的下标。
- 初始化和扩容
- 处理hash冲突
- 扩容
上面说的只是大体的几个重要的方面,下面按照源码一层层来解析
计算hash
//计算hash
// 这样做的目的是为了用最简单的尽可能的减少hash冲突
//
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
>>>和>>的区别都是移位运算,这里会涉及到
逻辑位移和算术位移。数字分为有符号和无符号。位移就是按照不同的方向将数字移动多少位,对于有符号和无符号数的移位操作,就涉及到下面两个概念:-
逻辑位移
对象:无符号数
逻辑右移动;
高位补0,低位剔除
逻辑左移动:
低位补0,高位剔除
-
算术位移
对象:有符号数(这里说的主要是负数,对于正数没啥差别)
逻辑右移动:
高位补充符号位,低位剔除
逻辑左移动:
低位补0,高位剔除
在java里面
<< : 逻辑左移动。
>>: 逻辑左移动。>>> : 算术右移动。
-
putVal
//方法参数说明
Params:
// hash – hash for key
// key – the key
// value – the value to put
// onlyIfAbsent – if true, don't change existing value(true,不会改变存在的值,这个参数的意思就是在key在hashmap之前存在的话,如果true就不会改变之前的值)
// evict – if false, the table is in creation mode.(这个参数很特殊,这可以实现在hashMap put值之后的后继操作,linkHashMap是通过这个值来实现LRU)
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就是hashMap里面的数组
// transient Node<K,V>[] table,首次使用的时候会初始化,长度的是2的n次幂的整数
if ((tab = table) == null || (n = tab.length) == 0)
// table没有初始化过,下面开始初始化
n = (tab = resize()).length;
// 通过(n - 1) & hash计算出要在table中放元素的位置,如果这个下标没有元素就直接构建Node节点,放进去就可以
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//下标有元素,从这里开始就解决hash冲突了
Node<K,V> e; K k;
// 数据下标这个元素,和要存放的元素的hash值一样,引用比较或者equals比较一样,那就说明是同一个对象,将 p(数据下标算出的这个元素)指向e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是数节点,那就说明已经变为红黑树了,此时,就要在红黑树中存放元素了。(到这里说明了p和要存放的元素不是一个对象)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 还是链表,还没有转换为红黑树,这里就需要构建节点,放在链表中,在判断链表节点的长度是否大于等于TREEIFY_THRESHOLD-1,如果是,就需要转换为红黑树
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;
}
//在循环遍历的时候,也需要判断链表中节点是否和 k一样。(hash一样并且,引用一样或者equal一样)
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;
//这也是拓展点,LinkedHashMap后置回调
afterNodeAccess(e);
// 这里要注意,一旦找到了key在之前是存放过的,put方法就会返回之前的值。
return oldValue;
}
}
++modCount;
//是否需要扩容, 负载因子 初始大小
//threshold = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//threshold是HashMap的一个int属性,int的默认值是0,在 resize()方法里面对threshold赋值计算了。
if (++size > threshold)
resize();
//这也是拓展点,LinkedHashMap后置回调
afterNodeInsertion(evict);
return null;
}
从这个方法就可以看出hashMap整个put流程
- 计算hash
- 判断table是否初始化过,如果没有,就初始化
- 计算要存在table中的位置,判断此位置是否有值,如果有,通过hash和引用和equals来判断是否是同一个元素。
- 如果不是,就看该节点是否是一个树节点,如果是,就交给红黑树来处理
- 如果不是,就遍历链表,采用尾插法,构建新节点,放在链表末尾。中途判断链表中的每个节点是否和key一样。如果是,跳出循环。
- 如果该key在链表或者红黑树中找到了,onlyIfAbsent是否要覆盖旧值。返回老值
- ++size 判断是否大于threshold,扩容
- 留给linkhashMap的回调
resize()
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) {
//MAXIMUM_CAPACITY = 1<<30
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新容量 = 旧容量的2倍并且新容量小于MAXIMUM_CAPACITY,
//并且旧容量已经大于DEFAULT_INITIAL_CAPACITY(1<<4=16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的阈值 = 旧阈值*2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
/*
1. 通过hashmap指定初始容量和加载因子的时候,oldThr就赋值。并且他的值就是通过构造方法指定的初始容量的值附近2的n次幂的整数的数字。
*/
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化
//DEFAULT_INITIAL_CAPACITY = 16
newCap = DEFAULT_INITIAL_CAPACITY;
//newThr = 0.75 * 16 = 12
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组成的数组
// 这个node是hashMap的静态内部类,实现了Map.Entry<K,V> 接口
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//初始化到这就结束了,将构建好的table数组返回就好了
//下面就是扩容的过程了
if (oldTab != null) {
//遍历之前的老table,如果数组下标位置有值的话,才会才做,否在不会操作
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 下标有值,将这个值复制给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//这意思就是e上没有hash冲突
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;
//计算是否要放在低位,即就是通过这样的计算之后,e的hash值没有超过
// oldCap ,没有就说明在新的table里面,key的位置还是和之前的位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//说明key的hash超过了oldCap,把这样的key放在高位组成的链表中
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//低位的链表存放的位置和新table存放的位置一样
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//高位存放的位置 就是原来的位置+旧oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结:
- 新的容量是原来的2倍,并且如果容量大于等于16,阈值也变为之前的2倍
- 容量大于MAXIMUM_CAPACITY,阈值为1<<30。并且不会扩容
- 通过构造函数指定容量和负载因子,开始的时候阈值是
容量附近的2次幂整数,在resize方法的时候会通过容量和负载因子重新计算阈值- 扩容的时候容量变为2倍,循环原来的table,按照
e.hash & oldCap) == 0分为两种情况。
- 为0说明原来的hash值,并没有超过table的容量,所以,这个hashKey存放的位置和在原来表中存放的位置一样。
- 不等于0说明,key的hash值超过了table的容量,在新表中存放的位置就是原来的位置 + 老table的容量。
问题:
- 如果在存放高位链表的时候。要存放的位置有值,看代码里面写的会直接覆盖?那么会不会出现这种情况呢?
treeifyBin(Node<K,V>[] tab, int hash)
//过渡方法,重点是 hd.treeify(tab);
//
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果长度还不足64,就扩容
// static final int MIN_TREEIFY_CAPACITY = 64;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//存放的为位置有值
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//替换,将node变为treeNode,将之前的node组成的单链表组成由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);
//将table中的index 赋值为 hd,hd就是由TreeNode组装起来的双向链表
if ((tab[index] = hd) != null)
//这是重点,树化
hd.treeify(tab);
}
}
// 这个方法是TreeNode的方法
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//获取当前节点的下一个节点
next = (TreeNode<K,V>)x.next;
// 将当前节点的左子树和右子树赋值为null
x.left = x.right = null;
if (root == null) {
//确定root,
//也就是说,这个方法一上来,第一次调用,第一次循环,确立了根节点。
x.parent = null;
//不是红节点
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//遍历root,这个root就是红黑树的根
for (TreeNode<K,V> p = root;;) {
//dir为<=0表示left,左子树比根节点大
//dir>0表示right, 右子树比根节点大
//pb存放的是根节点的hash值
int dir, ph;
K pk = p.key;
//这里就开始判断了
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//知识点,如果hash一样,不能确立左子树还是右子树,就会判断key是否实现了
//Comparable接口。
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//如果没有实现Comparable接口或者Comparable接口返回是0。那么就会随机比较一下
//首先会比较Class名字,如果也一样,就System.identityHashCode(a)
dir = tieBreakOrder(k, pk);
// 通过之前dir,来判断连接左子树还是右子树。
TreeNode<K,V> xp = p;
//注意这里的引用关系的改变,p=p.left或者right,如果已经有左子树或者右子树了,p下顺,继续循环,继续比较。
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(tab, root);
}
总结:
- 通过hash来比较是要存放在左子树还是右子树,如果hash相同,通过Comparable来确认
- 在插入的时候会调用 balanceInsertion,保持平衡
- 关于红黑树的部分在
红黑树章节介绍,在上面的例子中主要就是balanceInsertion方法
3. 数据结构
底层数据结构
数据 + 链表
Node节点组成的链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
TreeNode
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;
Entry<K,V> before, after;
}
4. 扩容
有了整体流程的分析,这里的答案就很确认了
newCap = oldCap << 1 //左移一位, 就是原来的容量*2
5. 红黑树
特点:
(1) 每个节点或者是黑色,或者是红色。 (2) 根节点是黑色。 (3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!] (4) 如果一个节点是红色的,则它的子节点必须是黑色的。 (5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
这个博客红黑树的删除和新增的每个步骤写的都很明确,建议,按照下面的列子,debug balanceInsertion方法画出红黑树比较直观的理解
HashMap<A, Integer> hashMap = new HashMap<>();
hashMap.put(new A(),1);
hashMap.put(new A(),2);
hashMap.put(new A(),3);
hashMap.put(new A(),4);
hashMap.put(new A(),5);
hashMap.put(new A(),6);
hashMap.put(new A(),7);
hashMap.put(new A(),8);
hashMap.put(new A(),9);
hashMap.put(new A(),10);
hashMap.put(new A(),11);
static class A{
@Override
public int hashCode() {
return 1;
}
}
6. 解决hash冲突的方式
数组 + 红黑树
7. 查找
public V get(Object key) {
Node<K,V> e;
// 先计算hash,和存放的时候是一直的算法
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;
//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)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//这种就是链表循环比较了,
// 先比较hash,然后通过引用或者equals比较来确定是否一致。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
((TreeNode<K,V>)first).getTreeNode(hash, key)
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
//通过hash值来比较,是按照左边查询还是右边查询
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
//如果一样,并且kc = null,就调用comparable接口
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//右子树优先
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
总结
查找分为两种
- 链表查询
- 循环比较,并且会通过 key的hash和 引用或者equals中的一个相等来比较。
- 红黑树查询
- 通过hash比较来确定是查找顺序(这个有待研究,这个好像不对,看代码这里的逻辑应该是先是右子树优先,通过判断来比较顺序,这样说好像也没有问题)
- 如果hash一样,并且也没有实现comparable接口,那就默认右子树优先,这样造成的结果就是遍历整个树。
- 可以看到在构建树的时候
system.identityHashCode(a)生成的随机数,只是单纯的构建树,在查找的时候完全没有用处。
8. 移除key remove
public V remove(Object key) {
Node<K,V> e;
//肯定先上来就计算hash
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//操作分为两步 1.先找到(红黑树的查找和链表的查找和数组的查找),2. 然后删除掉。删除也分为三步。红黑树的删除和链表的删除和数组的置空。
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;
//计算在链表中的位置
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;
//快速失败(记录hashMap remove的操作数)
++modCount;
--size;
//留给linkHashMap的回调
afterNodeRemoval(node);
return node;
}
}
return null;
}
9. 几个特殊的例子
hashcode值一样,equal方法没重写(不一样),并且链表变为红黑树,那么红黑树在构建子节点的时候改怎么组装值?查找的时候,怎么查找?
-
组装
例子:
HashMap<A, Integer> hashMap = new HashMap<>(); hashMap.put(new A(),1); hashMap.put(new A(),2); hashMap.put(new A(),3); hashMap.put(new A(),4); hashMap.put(new A(),5); hashMap.put(new A(),6); hashMap.put(new A(),7); hashMap.put(new A(),8); hashMap.put(new A(),9); hashMap.put(new A(),10); hashMap.put(new A(),11); static class A{ @Override public int hashCode() { return 1; } }这种情况下,hashCode都是一致的,就会产生hash冲突。
这个答案在
整体流程里面已经写清楚了,会按照hash值来比较,hash值一样,会通过Comparable接口比较,如果没有实现,就会比较Class的名字来比较,要是名字都一样,就通过System.identityHashCode来比较。 -
查找
如果是红黑树,那就按照二叉排序树的来查找,如果是链表就循环查找。
但是如果上面的按照上面的例子一样,hashCode都是1,但是没有重写equals方法,这个时候查找应该怎么做呢?
和上面
查找部分的一样。