jdk1.8的hashmap 和 1.7的一样都是实现一个map接口
包含了一些操作kv键值的一些常用的方法,get(),put(),remove()等等。
依然使用一个Set集合来保存所有的key,保证key的唯一性
迭代器就不介绍了。相较于jdk1.7,这里的出现了两个内部类,node和treeNode。
Node
点进去,这不就是jdk1.7版本中的entry嘛,这里只是修改了一下名字而已。功能也是一样的,用来保存每个kv键值对,next用于哈希冲突时,链接下一个Node。
parent 父节点
left 左子节点
right 右子节点
red 判断是否时红黑树
pre 指向链表的前一个节点
这里比较令人好奇的是红黑树中为什么会需要pre节点?这个不是用于双向链表中吗?
进入继承类中
在进入
到这里就知道答案了,TreeNode 内部隐式的继承了Node节点,因为node是单链表,只有next,这里存在一个pre,在内部隐式的构成了以双向链表。 —— 关于有什么作用后面在分析。
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/ kv的set集合
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
哈希表中实际的键值对的数量
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
哈希表的修改次数 —— 用于迭代器时,方式在迭代的过程中删除哈希表中的数据做判断
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
阙值 —— 用于扩容的时的判断
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
负载因子,用于计算阙值
final float loadFactor;
分析可以知道,jdk8的成员和jdk7的基本是一致的。
默认初始容量
最大容量
负载因子
上面的三个属性没有变化
但是在jdk8中多个3个默认的属性
TREEIFY_THRESHOLD = 8 表示树形的阙值
就是哈希表中的某个槽位上的Node的数量大于8时,就会将其由链表变为红黑树
UNTREEIFY_THRESHOLD = 6
取消树形化的阙值,红黑树种的节点的个数小于6时,就会将红黑树变为链表
MIN_TREEIFY_CAPACITY = 64
树化时,哈希表的最小容量,因为树化完成之后,可能还会将红黑树转化为链表,转化过程种设计到槽位的重新计算,位置重新分配,保证哈希表种有足够的槽位
可以看到依然是只有四个,所以这里就直接分析默认构造方法。
看到这里,忍不住笑了。jdkb做的比jdk7更绝,初始化时,就只是设置了一个默认的负载因子,jdk7好歹还设置了一下阙值,针对不同的实现,调用了一下init()方法。
然后看一下有参数的构造方法
参数值为初始化容量的大小,然后调用了另一个构成方法
这里就基本和jdk7一致,设置了一些基本的属性,包括阙值,负载因子,重点注意,这里依然没有对哈希表进行实例化(分配空间),真正的实例化过程在put()时才做。 好处就是,使用的懒加载机制,到正真使用哈希表时才初始化哈希表,提高空间利用率。
不同于jdk1.7 直接在方法内部处理,这里是调用一个方法来处理put()。首先计算了一下hash值
计算的方式页没有1.7版本的复杂,直接调用了Object 对象的hashcod()函数得到哈希值
进入hashcode() 发现是一个native方法,所以可以知道计算hashcode值不是有java代码实现,而是c++ 实现。实现得原理是根据该对象在内存中得位置来计算得。 见下图
为什么不是用java 来实现,可能就是因为java操作内存比较慢,所以使用c++来实现。
计算得到得哈希值,然后向右逻辑循环右移了16位,使用高16位来作为哈希值,这种做法有个名字 —— 扰动函数
计算完哈希值,后进入方法体。 代码比较复杂,这里注释一些关键点
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
首先判断哈希表是否位null,在前面构造函数中并没有初始化哈希表,这里需要初始化哈希表
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);
else {
Node<K,V> e; K k;
判断一
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;
}
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;
}
代码做了一下几件事
1.如果哈希表为空,就初始化哈希表
2.根据哈希值计算槽位
3.三个判断
如果当前槽位为null,则直接将新的节点插入到对应槽位上
如果当前节点为红黑树节点。则将该节点插入大该红黑树中
如果该节点是链表节点,就将节点插入到链表中
4.判断待插入的新节点是否存在,如果存在就将新节点的值插入到该节点中,然后返回旧节点的值
5.modCount++ 操作的次数自增 —— 用于迭代器
插入到红黑树节点分析
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
可以看到整个插入的关键代码是在一个for中完成的。
这段代码主要就做了一件事,判断红黑树中是否存在该待插入的键,查找的过程是通过哈希值的大小来判断的,因为在存储的时候,使用的也是哈希值的大小关系来存储的,然后通过比较key得到判断的结果。
这段代码就是插入节点的真正的逻辑
分析可以知道:
这里在插入新节点时,在两个地方进行了插入,首先是将新节点祖父节点的下一个节点(链表),将新节点插入到红黑树的左节点或者右节点(红黑树)
注意图中划线的方法就是树结构的调节 —— 在红黑树原理篇中讲过树结构调节的原理。 代码有点复杂,懒得分析了(哈哈)。总之就是原理篇中讲过的各种情况,变色 + 旋转
到这了put()方法就分析完了,代码有点长,这里做个小结
首先是判断哈希表是否为null,根据情况创建哈希表,接着通过哈希值计算槽位值,然后是插入数据的三中情况,如果槽位值为null,则直接将新的节点插入,如果不为null,如果为红黑树,就使用红黑树的方式插入新的接待你,如果是链表结构,就使用链表的方式来插入新的节点。
计算哈希值,然后进入getNode()
分析:通过哈希值计算槽位,判断槽位第一个元素是不是所需的节点,如果不是就判断第一个节点是红黑树还是链表节点,如果是红黑树,就使用查找红黑树的节点的方式来查找(查找过程类似于二叉搜索树),反之则使用循环遍历链表得到所需节点。
计算哈希值
删除方法分为两部分
首先是找到需要被删除的节点
寻找的方式也是根据节点的类型来查找(红黑树,链表)
这里的node节点就是找到待删除的节点。如果是树节点,则使用红黑树删除的方式来删除,如果是链表就使用链表的结构来删除
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;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
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;
while ((sl = s.left) != null) // find successor
s = sl;
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
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = 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;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
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;
}
}
if (movable)
moveRootToFront(tab, r);
}
可以看到删除的代码是真的有点长,且不好理解,所以大致了解一下就可以了(感觉也记不住)。分析几个关键点
取消树化
root 是根节点,当根节点为null,或者根节点的左节点或者右节点为null,或者左节点的左节点为null时就取消树化。
链表删除节点
这里的p表示的是node的父节点,删除时直接将父节点指向当前节点的下一个节点就好了。当前节点失去引用,等待垃圾GC回收。
分析方法得注释
该方法即可以用来初始化哈希表,也可以用来对哈希表进行扩容。注意区别:在jdk1.7中 初始化哈希表和扩容是两个不同得方法;
jdk1.7 初始胡哈希表
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 = 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];
table = newTab;
这里oldTable 为null 所以不进入直接返回
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;
}
代码有点长,分块来分析
首先是计算newCapacity,大小为原来的两倍,并创建新的哈希表
oldtable != null 使用来判断是初始化哈希表还是扩容
这里依然是分为链表和红黑树的不同情况单独进行扩容
这里是取消树化的具体逻辑,
默认值为6,就是锁如果高位或者低位中的节点的个数 < 6 就会 进行取消树化
取消的逻辑
就是通过树型节点 创建一个新的链表节点,形成单链表
如果高位或者低位的节点的个数大于6,就会重新进行树化,如果高位和低位都大于6,那么就会形成两颗新的红黑树。
小结:
可以发现,在扩容时,无论时红黑树还是链表,操作方式都是类似的,首先将 红黑树或者链表,拆分为 高位链表 和 低位链表,然后再进行一变换 存入新的哈希表中。
可以发现jdk1.8 版本的哈希表相对于1.7 真的是复杂了不少,但是其方法还是有规律。针对红黑树和链表 做了不同的处理,链表变为红黑树,红黑树变为链表 。
最后还有一个知识点: 就是jdk1.8 版本中是没有循环链出现的,但是也会有新的问题,就是值覆盖的问题。