简介:
前两篇文章,我们分析了数组和链表,我们看到了数组和链表都有相应的缺点。有没有集两家之长的呢?有,那就是HashMap。
说到HashMap,不得不说一下hash,也叫散列。就是把任意长度的值,通过散列算法,得到一个固定长度的值。常见的几种Hash函数有直接寻址法、平方取中法、 除留余数法…
我们简单看下String的hashCode():
比较简单,遍历字符串,取每个字符串的ASCII值 * 31的n次幂,至于为啥是31,就不解释了,网上有专业的解释,我也没看 懂,反正只要知道性能杠杆的就行了。
HashMap简单来说,就是用key的哈希值去计算出在数组下标的位置,不考虑hash冲突的情况,只需要一次即可定位。当然冲突避免不了,而HashMap采用的是链地址法解决的,冲突了之后往后挂。本篇文章是基于jdk1.8。
//序列号ID
private static final long serialVersionUID = 362498820763181265L;
//默认的容量,也就是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,数组的长度 * 0.75,得到的就是临界值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//阈值,超过这个阈值就会转成Tree结构
static final int TREEIFY_THRESHOLD = 8;
//阈值,低于这个阈值就会转成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//总元素的上限,超过这个上限,也会转成Tree结构
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组
transient Node<K,V>[] table;
//另一种存储结构,遍历用的。
transient Set<Map.Entry<K,V>> entrySet;
//元素总个数
transient int size;
//修改次数
transient int modCount;
//临界值,一般等于table的长度的0.75
//如果初始化的时候指定了长度,指定的长度就是临界值,不用 * 0.75。
int threshold;
//负载因子,默认是0.75,数组长度 * 负载因子等于临界值。
final float loadFactor;
数据结构:
大概就是这个样子,数组的下标的节点有的是链表,有的是红黑树,具体看数量。
构造方法:
方法1很简单,方法2调的方法3,我们就先看下方法3。
方法3也很简单,就一个方法tableSizeFor(initialCapacity),计算传进来的初始长度是否的2的n次方,不是就改成是,我们来看下。
可以看到,经过一系列的计算,至于为什么这么计算我们就不深究了,但是最后的结果肯定是2的n次方,至于原因,下面会说。
再看下方法4,主要是putMapEntries(m, false),来看下:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//先得到m的元素数量,这里以HashMap为例
int s = m.size();
if (s > 0) {
if (table == null) {
//算出负载因子
float ft = ((float)s / loadFactor) + 1.0F;
//判断负载因子是否大于最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//初始化的时候调用这个方法,threshold肯定为0,必定满足条件
//相当于以丢进来的HashMap的元素个数为初始化长度
if (t > threshold)
//确定下临界值
threshold = tableSizeFor(t);
}
//判断元素个数是否大于临界值
else if (s > threshold)
//扩容
resize();
//遍历元素,entrySet()后面说
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//循环put进map
putVal(hash(key), key, value, false, evict);
}
}
}
其实很简单,就是以丢进来的HashMap的元素个数为初始化长度,然后遍历元素,重新put。
下面看一下HashMap的方法,这里只分析常用的方法。
put(K key, V value):
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):
由于put里面包含的东西太多,下面我们把put方法拆开来说。
大致流程就是:
1:判断数组是否为null:
//tab相当于上面的table
//p是要插入的下标的头节点
//n是数组的长度
//i是计算出来要插入的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//扩容
n = (tab = resize()).length;
2:计算下标:
//用hash计算下标,判断当前下标是否为null
if ((p = tab[i = (n - 1) & hash]) == null)
3:判断当前下标是否为null:
//为null就直接new一个新节点
tab[i] = newNode(hash, key, value, null);
4:判断是否是重复的key:
Node<K,V> e; K k;
//判断hash是否相等,判断key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//都相等,说明是重复key,记住这个节点
e = p;
5:红黑树的新增:
else if (p instanceof TreeNode)
//新增节点,新增成功会返回null。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
6:链表的新增:
for (int binCount = 0; ; ++binCount) {
//判断当前节点是否有下一个
if ((e = p.next) == null) {
//没有下一个就直接new一个新节点,把p的next指向这个新节点
p.next = newNode(hash, key, value, null);
//判断下节点数量是否大于阈值
if (binCount >= TREEIFY_THRESHOLD - 1)
//大于就要将链表转红黑树结构
treeifyBin(tab, hash);
break;
}
//判断hash是否相等,判断key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//都相等就说明找到一样的key了,跳出循环
break;
//e是当前节点的next,相当于下一个,此时的p就是下个节点,然后回上去继续判断。
p = e;
}
7:判断是否需要替旧元素:
//判断e是否为null,e为null,就是添加成功,不为null就是不成功。
if (e != null) {
//不为null就是有重复key,取出旧值
V oldValue = e.value;
//onlyIfAbsent是判断是否覆盖,true就是不覆盖,false就是覆盖。
if (!onlyIfAbsent || oldValue == null)
//替换掉旧值
e.value = value;
//为LinkedHashMap服务的
afterNodeAccess(e);
return oldValue;
}
上面大概注释了步骤,有些步骤详细说一说:
流程1里面的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;
//判断老长度是否大于0,
//初始化的时候如果指定容量,仅仅指定了threshold,数组的长度还是0。
if (oldCap > 0) {
//老长度大于0,那么这就是真正的扩容,所以判断下是否大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//那么临界值也是最大容量
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap << 1相当于*2,如果*2之后小于最大容量并且大于等于初始化长度
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新临界值就是老临界值的2倍
newThr = oldThr << 1;
}
//判断老的临界值是否大于0
else if (oldThr > 0)
//如果老数组的长度是0,但是老临界值大于0,
//就说明初始化的时候指定了容量,这时候把初始化的容量赋给新数组的长度。
newCap = oldThr;
else {
//如果老数组的长度、老临界值都是0,就说明初始化的时候并没有给定容量
//那么新长度就是默认值,也就是16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的临界值也是16*0.75
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//再次判断下新临界值是否等于0
if (newThr == 0) {
//新临界值等于0,只有一种可能,那就是初始化的时候指定了容量,重新算下临界值。
//额,兜了一圈,临界值还是 * 0.75。
float ft = (float)newCap * loadFactor;
//把ft赋给新临界值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把新临界值赋给threshold
threshold = newThr;
//new一个新长度的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把新数组赋给table
table = newTab;
总结一下:
如果是老数组有值,那么新长度 = 老长度 * 2,新临界值 = 老临界值 * 2
如果是初始化指定了长度,新长度 = 给定的长度,新临界值 = 给定的长度 * 0.75
如果是初始化没有指定长度,新长度 = 默认长度16,新临界值 = 默认长度16 * 0.75
//判断老数组是否为null
if (oldTab != null) {
//不为null就遍历数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断每个下标是否为null
if ((e = oldTab[j]) != null) {
//不为null的话就先把老数组当前下标置为null,方便GC。
oldTab[j] = null;
//判断此下标的节点的下一个是否为null
if (e.next == null)
//为null就说明此下标只有一个元素,重新计算下标,赋给新数组。
newTab[e.hash & (newCap - 1)] = e;
比较简单,重新计算下标,赋给新数组。
红黑树的复制:
else if (e instanceof TreeNode)
//复制红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
看下split(),拆开来说:
//遍历当前红黑树
for (TreeNode<K,V> e = b, next; e != null; e = next) {
//得到当前节点的下一个
next = (TreeNode<K,V>)e.next;
//把当前节点的next置为null
e.next = null;
//判断是否需要改变下标,具体前面说过了
if ((e.hash & bit) == 0) {
//判断上一个是否为null
if ((e.prev = loTail) == null)
//为null此时的节点就是根节点,把e赋给loHead
loHead = e;
else
//loTail此时是当前节点的上一个把loTail的next指向当前节点
loTail.next = e;
//把tail改成指向当前节点,其实就是一个临时变量,记住当前节点。
//然后下次遍历进来的时候,要把上个节点的next指向当前节点,提前保存一下。
loTail = e;
//不需要改变下标的次数自增
++lc;
}
细说:**if ((e.hash & bit) == 0)**这个判断,判断每个节点是否需要改变下标,等于0就不用改变下标:
HashMap每次扩容都是 * 2,先看直接计算的过程:
通过上面的计算过程,终于知道HashMap为什么要根据(e.hash & oldCap) 是否等于0判断要不要改变下标了。
那么为什么新下标 = 原下标 + 原数组长度呢?
因为数组长度是2的n次方,* 2之后,hash对应的那位bit如果是1,hash & oldCap -1的结果就是第n位多了1,等于 + 原数组长度。
再说复制节点:
链表的复制其实跟LinkedList的addAll()差不多,上篇文章已经详细描述过了,这里再简单配个图:
大家有没有觉得奇怪?这个是复制红黑树啊,怎么遍历的方法貌似就是遍历链表的方法呢?
不错,这里的确是遍历的链表。其实,尽管该下标的数据结构是红黑树,但是每个节点其实还是维护了一个next的,就是下个节点。
原因嘛,我估计:
1:是因为扩容之后,每个节点的下标可能会改变,而如果改变的节点较多,频繁的删除节点,红黑树的自我修正的次数会很多,浪费时间。所以这里干脆再维护一个链表,需要改变下标的是一个链表,不需要的是一个链表,然后重新转成红黑树。
2:是遍历的时候,就是取的每个节点的next遍历的,每个节点的next就是put进去的顺序。不过转红黑树的话,会把头节点改成root的,下面会讲。
else {
//跟上面的意思一样
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
//需要改变下标的次数自增
++hc;
}
//判断loHead是否为null,说明此下标有值并且不需要改变下标。
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
//转成链表
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null)
//转成红黑树
loHead.treeify(tab);
}
}
//跟上面的意思一样
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
//遍历
for (Node<K,V> q = this; q != null; q = q.next) {
//把TreeNode转成Node
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
比较简单,就是把TreeNode转成Node。
treeify()请看本人另一篇文章,因为篇幅较长,红黑树部分另写了一篇,地址:红黑树篇
流程2里面的计算下标:
if ((p = tab[i = (n - 1) & hash]) == null),这里解释一下为什么数组的长度是2的n次方。
1:当数组的长度是2的n次方时,(n - 1) & hash,会满足一个公式:(n - 1) & hash = hash % n,但是 & 比 % 计算快。
2:当数组的长度是2的n次方时,(n - 1)之后就变成0000011111这样的,尾端全是1。
可以看到结果取决于两个:一个是(n - 1)最后有x个1,一个是hash的后x位。
而数组的长度一般是不会超过2的16次方的,所以正常来说key.hashCode()的高16位进行&运算 是没啥用的。
我们假设数组的长度是默认长度16,16-1就是15,而hash是从1开始:
可以看到hash是什么结果就是什么,所以上面的(key.hashCode()) ^ (h >>> 16),就是为 了让hash的低16位更加均匀,减少冲突。只要hash足够均匀,那么计算出来的下标就是均匀的。
而数组的长度如果不是2的n次方,假设数组的长度是15,15-1就是14,而hash还是从1开始:
可以看到,结果很不均匀,并且有些值是永远得不到的。因为你前面有效位开始有0了,进行 &运算,0永远是0。
正常其实用%计算就行了,但是%计算慢,所以HashMap采用了&。
流程5的putTreeVal、流程6的treeifyBin在另一篇文章说。
get(Object key):
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果该下标不为null
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
//哈希值一样,并且key相等,就返回。
return first;
//上面的没有return,说明第一个节点不是要找的,开始往下找
if ((e = first.next) != null) {
//判断是不是红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不是红黑树,就是链表
do {
//判断每个节点的哈希值和key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//遍历链表
} while ((e = e.next) != null);
}
}
return null;
}
除了红黑树的getTreeNode另一篇文章说,其他的也比较简单。
remove(Object key):
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node
boolean matchValue, boolean movable) {
Node
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node
//如果哈希跟key都相等
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
else {
//链表的遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//找到这个节点就跳出循环
node = e;
break;
}
//p是node是上一个节点
p = e;
} while ((e = e.next) != null);
}
}
根据key去查找节点,getTreeNode在另一篇文章分析,找到了之后准备删除。
//如果找到那个节点,还要判断matchValue是true或false
//matchValue是false代表找到相同的key就可以删除
//是ture代表还要验证value是否一样,value相等才可以删除
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指向删除节点的next。
//跳过这个节点,这个节点就没了,相当于删除了。
p.next = node.next;
//操作次数自增
++modCount;
//数量自减
--size;
//为LinkedHashMap服务的
afterNodeRemoval(node);
return node;
}
removeTreeNode在另一篇文章分析,可以看到,真正删除之前还判断了一下matchValue这个参数,其实这就是remove(Object key)和remove(Object key, Object value)的区别。链表的删除其实也很简单,prev的next指向删除的节点的next,next的prev指向删除的节点的prev。
示意图:
remove(Object key, Object value)其实上面已经说过了,put、get、remove都说完了,接下来看看遍历。
for (Object key : map.keySet()) {
}
for (Map.Entry<String, String> entry : map.entrySet()){
}
大家用的比较多的应该是上面的2种遍历方式,大家应该知道,像上面的增强for的遍历,实际上相当于迭代器。
上面的代码可以转成这样的:
Set<Object> set = map.keySet();
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
}
Set<Map.Entry<Object, Object>> entries = map.entrySet();
Iterator<Map.Entry<Object, Object>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Object, Object> entry = iterator.next();
Object key = entry.getKey();
Object value = entry.getValue();
}
按照上面的迭代代码,我们先看keySet():
KeyIterator继承了HashIterator,继续看下HashIterator的代码:
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
//拿到modCount
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) {
//从0下标开始遍历,直到不为null的下标
//把值赋给next,这是第一个节点
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
//判断next不为null
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//这里可以看modCount的作用,如果此时的HashMap被别的线程修改了,这里会不相等,会报错。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//从第一个节点next开始取next.next,直到为null,就是把当前下标的节点从头往后取,直到为null
if ((next = (current = e).next) == null && (t = table) != null) {
//再从新的index往下遍历
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
很简单,就是先遍历下标,然后把 每个下标的所有结点全部取出来。
再看entrySet():
EntryIterator也是继承了HashIterator,只不过一个是nextNode().key,一个是nextNode()。所以建议使用entrySet,keySet都已经拿到node了,结果只返回了key,当然人家名字就叫keySet。
说到这里,终于把HashMap啃完了,太不容易了,下篇分析一下队列。