1.构造函数如下
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m)
2.底层实现思想
(1) 基于数组和链表实现,拉链法,数组存在扩容不存在缩容,链表在java8里加入了红黑树结构,见下文
(2) 在通过迭代器遍历HashMap的Node过程中,如果进行了结构性的更改,会fail-fast快失败,会抛出ConcurrentModificationException,api doc讲到依赖这个异常去纠错是不合理的,而是应该仅仅是去发现bugs
一、常见问题解答(大多是api doc上的原话或是看源码自己的总结)
1.扩容机制
翻倍扩容,容量变成原来的两倍,默认容量为16,也可初始指定容量,初始化的时候,阈值threshold为capacity,也就是说第一次threshold并不是等于capacity*loadFactor,当元素个数到达threshold时会扩容,即resize
2.负载因子为什么默认取0.75
负载因子高,空间利用率高,但是查询效率低,容易哈希碰撞。负载因子低,空间利用低,浪费空间。因此折中
另外,只有哈希碰撞严重时,才会出现红黑树结构,红黑树节点占用空间大约是普通节点2倍,实际上很少出现
理想情况随机哈希下,节点bucket上出现node个数和概率满足泊松分布,当负载因子是0.75时,泊松分布的lambda值是0.5,每个bucket链表节点node出现的个数和概率满足一个公式(见api doc)
当出现8-9个node时的概率,算法理想上只有千万分之1,即 1 in ten million
3.哈希函数是怎么设计的,哈希是怎么定址的
使用key.hashCode的高16位保持不变,低16位为高16位和低16位的异或,即(h = key.hashCode()) ^ (h >>> 16)
原因:通常的hashCode函数已经足够合理分布了,我们没有必要打乱他的节奏,考虑到位运算的便捷和快速,减少系统损耗,容量又是2的幂,哈希值只是比特位不一样,因此我们用高位和低位异或,加入对高位的影响
而哈希碰撞用红黑树处理。哈希定址采用哈希值对容量取模,源码中是通过与capacity-1进行位运算,因为位运算快
4.如果哈希碰撞严重,可能有哪些原因
可能是重写Key的哈希函数设计的不合理,尽量用Objects.hash即可,系统自带的。再就可能是负载因子设置的过高
5.链表和红黑树转换的规则是什么样的
每个桶上链表元素个数>=8个元素则转换为红黑树存储(并且满足capacity>=64),减少到<=6个又变回链表结构(扩容时,可能一条链变两条链,所以元素个数会减少),删除结点时,变回链表的触发条件因树的结构而异,此时树大概只有2-6个node,源码中的条件是root的左儿子的左儿子为空,具体源码注释有讲到
6.java7的HashMap和java8的HashMap有哪些区别
java8源码就增加了几千行,加入了很多默认函数,lambda等,更重要的是java7插入节点使用头插法(会产生环形链表死循环问题)和java8使用尾插法,哈希定址计算方式也不一样,并且java8引入了红黑树,这是java7不具备的。
7.java8的HashMap为什么也不是线程安全的
resize函数就是不安全的,还没复制完,另一个线程访问,此时table部分bin为null,源码中的处理是先开辟新数组,再复制元素。
put的时候也会出现问题,bin有值,然而读不到,本该形成链表,结果覆盖了另一个线程新put的值
size变量也不是volatile线程可见的,而且有++size操作
8.HashMap里结点Node的结构是什么样的
Map.Entry是Map接口里的public内部子接口
HashMap.Node是普通链表节点,是内部类,它实现了Map.Entry
HashMap.TreeNode是红黑树节点,是final内部类,它继承了LinkedHashMap.Entry,而后者又继承了HashMap.Node
9.Hashtable和HashMap的区别
一个区别是线程安全性,一个是key和value是否可以为null
10.HashMap.Node的hash属性和key属性为什么是final的
因为不能改变,hash不用重复计算,节约计算代价
11.红黑树的排序规则是怎么样的
红黑树的排序首先使用hash值比较,其次是Key的compareTo方法(判断实现Comparable接口),再是类名字符串等(如通过反射)和identityHashCode值排序,构造红黑树
二、源码解析
1.哈希计算方法
/**
* 哈希计算规则
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.容量capacity计算规则
/**
* 打成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;
}
3.put方法底层原理
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
//该bin是空,直接添加,注意哈希定址是位运算,i = (n - 1) & hash
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//key已存在,直接找到,就替换
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;
}
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;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//到达阈值,扩容
resize();
afterNodeInsertion(evict);
return null;
}
4.扩容函数
/**
* 扩容机制
* @return the table
*/
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;
}
//位运算capacity进行翻倍
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 = oldThr;
else { // zero initial threshold signifies using defaults
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];//开辟新数组
//记住,此时数组替换了,node元素还没过来
table = newTab;
if (oldTab != null) {
//开始把旧数组的node元素复制到新数组中
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;
//敲黑板,这里只判断最高位,如果不为0,那么hash值大于旧的容量,要放到高位的链表中,
//这就是扩容为什么一条链表可能变2条的原因
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;
//魅力之处,直接+oldCap定址,这是与java7的一个不同点
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新开辟的数组
return newTab;
}
5.把hashmap对应哈希值位置的bucket变成红黑树
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);
//最后调用头结点的treeify方法,将其转变为具备父子关系的红黑树结构
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
三、红黑树结点内部的方法
1.把树的root放到链表的头

/**
* 把root放到链表的头部去
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
2.红黑树的二叉查找
/**
* 从当前TreeNode往子孙节点搜索k
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
//红黑树是二叉有序的,二分搜索,log(n)复杂度
int ph, dir; K pk;
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)
//如果左边null,则到右边搜
p = pr;
else if (pr == null)
//如果右边null,则到左边搜
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//如果能通过compareTo判断,则这样继续搜索
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
//否则类似递归,继续find从右儿子搜索,直到找到
return q;
else
//如果右儿子搜索没搜到,继续从左儿子往下搜
p = pl;
} while (p != null);
return null;
}
3.建立红黑树,key怎么比较大小
//首先通过哈希值比较,然后通过key的compareTo方法,如果都无法比较大小,那么采用下面的方法比较大小
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
//如果通过类名字符串无法区分,用identityHashCode应该能区分吧,这是内存级别的
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
4.把红黑树打成链表
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) {
//把树节点转换为普通链表节点,然后next串起来,prev之前有值不变
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
//返回头节点
return hd;
}
//真的佩服源码的规范性,一般t代表temp临时变量,l代表左边,r代表右边,h代表head或high
//还有比如,hd代表head,tail代表尾巴,tl代表temp临时变量,等等
5.红黑树删除节点(不包含颜色调整),先看图解,如果不懂原理请后续关注我的红黑树基础-第二篇
这个函数主要是this是要删除的节点,找到节点s与之互换,然后用balanceDeletion调整,看下图中树结构的变化再看代码注释

/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
* @param map 该hashmap
* @param tab hashmap内部Node数组
* @param movable 是否需要移动root到链表头
* @param this 待删除节点p
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
//root是根节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//分别是this=p的链表指针的前驱和后继
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
//如果要删除的p是链表的头,那么first = succ;并且tab[index] = succ;
tab[index] = first = succ;
else
//否则链表断开this的链接
pred.next = succ;
if (succ != null)
//链表断开this的链接,把链表关系完善
succ.prev = pred;
if (first == null)
//空树
return;
if (root.parent != null)
//获取到真正的根root
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
//根的左孩子的左孩子为空,基本上可以判断只剩2-6个node了,红黑树可以变成链表了
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//主要关注这种左右孩子都非空的场景,为什么这种这么复杂,请看我写的关于红黑树基础的其他博客
if (pl != null && pr != null) {
//请看图解,我随便画了个图,节点名和变量名一致
TreeNode<K,V> s = pr, sl;
//先找到大于删除节点p的最小节点,为什么这么做,请看红黑树基础-第3篇
while ((sl = s.left) != null) // find successor
s = sl;
//首先互换p节点和s节点的颜色,因为最终s要被换到p的位置,p要被换到s的位置
//互换后,在p位置的s因为是p的颜色,所以不影响红黑树的性质
//而换到s位置的p,颜色是原s节点的颜色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
//此时s==sp==pr,其实是建立p和s的关系,将else的内容简化了
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
//建立p和sp的新父子关系,大家可以画图分析
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
//建立s和pr的新父子关系
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
//建立p和sr的新父子关系
if ((p.right = sr) != null)
sr.parent = p;
//建立s和pl的新父子关系
if ((s.left = pl) != null)
pl.parent = s;
//建立s和pp的新父子关系
//如果pp为空,之前p就是根节点,那么现在s就是根节点了
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
//如果原来p是pp的左孩子,互换后s就还是pp左孩子
pp.left = s;
else
pp.right = s;
//如果sr不为空,则互换后p有右孩子,没有左孩子,
if (sr != null)
//单链接情况直接用孩子替换
replacement = sr;
else
//此时p没有孩子
replacement = p;
}
//单链接情况,单单只有左孩子
else if (pl != null)
replacement = pl;
//单链接情况,单单只有右孩子
else if (pr != null)
replacement = pr;
else
//p是叶子结点
replacement = p;
//互换后,如果p不是叶子结点,在树结构中直接把p节点干掉
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//回到红黑树的平衡删除了,如果要删除的节点是红色,那么直接删除即可
//如果p是黑色的,那么此时就不满足红黑树的性质了,因为少了一个黑色节点,那么要进行balanceDeletion调整
//注意:p的颜色是原来s的颜色
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//那么如果p是叶子结点,在树结构中直接把p节点干掉,detach断开连接
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
//是否需要把root放到链表head
if (movable)
moveRootToFront(tab, r);
}
6.其他部分函数未完待续
红黑树源码部分(左右旋转,平衡插入和平衡删除请看我第三篇红黑树源码解析)
本文深入探讨Java中HashMap的实现机制,包括构造函数、底层思想、扩容机制、负载因子选择、哈希函数设计、链表与红黑树转换规则,以及与Java 7 HashMap的区别。同时,解析了HashMap线程安全问题、节点结构、红黑树排序规则等关键概念。
3726

被折叠的 条评论
为什么被折叠?



