HashMap 1.8 源码分析
HashMap简介
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
Hash算法简单认识
简单介绍一下Hash算法的原理,深入探究请移步隔壁
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
Hash算法特点
- Hash算法可以将一个数据转换为一个标志,这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点,就是很难找到逆向规律。
- Hash算法是一个广义的算法,也可以认为是一种思想,使用Hash算法可以提高存储空间的利用率,可以提高数据的查询效率,也可以做数字签名来保障数据传递的安全性。所以Hash算法被广泛地应用在互联网应用中。
- Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。
影响产生冲突多少有以下三个因素:
- 散列函数是否均匀;
- 处理冲突的方法;
- 散列表的装填因子。
HashMap静态变量
变量定义 | 解释 |
---|---|
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; | 初始容量设置为16,必须是2的幂 |
static final int MAXIMUM_CAPACITY = 1 << 30; | 最大容量 |
static final float DEFAULT_LOAD_FACTOR = 0.75f; | 在构造函数中未指定时使用的负载系数。 |
static final int TREEIFY_THRESHOLD = 8; | 当链表长度达到阈值,转换为红黑树 |
static final int UNTREEIFY_THRESHOLD = 6; | 当红黑树节点小于阈值,转换为链表 |
static final int MIN_TREEIFY_CAPACITY = 64; | 初始设置红黑树最小容量,最小为 4 * TREEIFY_THRESHOLD |
transient Node<K,V>[] table; | 存储HashMap的table的结构,最外层是数组,数组里包含链表 |
transient Set<Map.Entry<K,V>> entrySet; | 保存缓存的entrySet。请注意,使用AbstractMap字段用于keySet和values |
transient int size; | table中键值对数量,不为空的数量(不是数组大小) |
transient int modCount; | 已对HashMap进行结构修改的次数(put()的时候也计算+1) |
int threshold; | 要调整大小的下一个大小值(容量*负载系数),扩容阈值 |
final float loadFactor; | 哈希表的负载因子。未指定时 = DEFAULT_LOAD_FACTOR |
基础HashMap:数据结构
基础链表结构,结构中有4个值
1、hash:通过key计算的hashCode
2、key:当前存储的Key
3、value:value
4、next:指向下一个节点的引用
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap红黑树:数据结构
1、红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组
2、红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能
3、后面会详细讲解红黑树
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
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);
}
}
//.........此处省略部分代码
}
Hash.key计算方式
通过put中的key计算出的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap.put()
put的时候,有三个判断
1、if ((tab = table) == null || (n = tab.length) == 0),table为空,则整理table
2、if ((p = tab[i = (n - 1) & hash]) == null) ,当前hash计算数组的位置中是否为空,或新建链表
3、else 代码块中,还有3个判断
3.1、判断hash和Key是否同一个节点并覆盖其值
3.2、判断是否红黑树,进行红黑树操作
3.3、 链表中找到hash和key相同的,覆盖呗。说明HashMap在计算hash的时候,相同hash放在同一个链表,然后在使用hash和key再定位
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 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;
// tab赋值并判断是否为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 重新整理tab
if ((p = tab[i = (n - 1) & hash]) == null) //计算hash,并给p赋值,判断该位置是否为空
tab[i] = newNode(hash, key, value, null); // 为该位置插入第一个值
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循环遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 找到指向下为null的位置,新增插入节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //该链表中是否满足转化树形条件
treeifyBin(tab, hash); // 转化红黑树结构
break;
}
// 链表中找到hash和key相同的,覆盖呗。
// 说明HashMap在计算hash的时候,相同hash放在同一个链表,然后在使用hash和key再定位
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); // 将节点e移动到最后面
return oldValue;
}
}
++modCount; // 增加修改次数
if (++size > threshold) // 若table.size +1 大于扩容阈值,重新整理table或扩容
resize();
afterNodeInsertion(evict); // 是否创建模式,true则可能删除头节点,并移动节点位置
return null;
}
HashMap.get()
1、先通过hash计算获取数组中是否不为空
2、然后判断数组中链表第一位是否能匹配
3、判断是否红黑树查找 或 循环遍历链表寻找
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// table不为空,且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 {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e; // 循环遍历查找链表
} while ((e = e.next) != null);
}
}
return null; // 找不到就直接返回null
}
前置HashMap中的get()和set()方法
红黑树简介
1、红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组
2、红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能
3、红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树
在二叉树查找树的基础上,再加上以下特性:
- 节点是红色或黑色。
- 根节点是黑色。
- 所有叶子都是黑色。(叶子是NIL节点)
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
二叉树在查询中的缺陷
1、二叉树在查询的时候,若是左右子树都比较均匀的情况下,即相对对称。查询效率比较高
2、若二叉树是节点比较深的左斜树或者右斜树,或子节点有左斜树或右斜树,那么在查询的时候,就需要遍历更深的节点。这不符合更优的查询效率
3、红黑树就是由此诞生出来的平衡二叉树,通过旋转来平衡树的深度,并保证还是二叉查找树,以此平衡查询效率
查询效率较低的斜树:
红黑树左旋和右旋
五大特性:
- 节点是红色或黑色。
- 根节点是黑色。
- 所有叶子都是黑色。(叶子是NIL节点)
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
红黑树是通过旋转来保持满足红黑树的五大特性
原因是在插入新节点的时候,颜色不同,层级不同,会破坏红黑树五大特性
下面使用同一颗二叉树进行左旋与右旋操作
root A树进行左旋操作
root A是左旋之前的树
以A节点开始左旋,将A的右节点(C节点)提升为根节点
A节点的上级节点设置为C节点,C节点断开F节点的连线
A的右节点设置为F节点
注:若B节点下的子节点挪到G节点下,会比较平衡,但为了方便理解,用相同一棵树进行左右旋转
root A树进行右旋操作
root A是右旋之前的树
将B节点提升为根节点,A节点设置为B节点的右节点
E节点设置为A节点的左节点
注:若C节点下的子节点挪到D节点下,会比较平衡,但为了方便理解,用相同一棵树进行左右旋转
TreeNode进行旋转平衡
旋转平衡的时候,暂无需注意红黑变色
仅需进行树的旋转即可,外部会继续处理红黑变色问题
/**
* 左旋方法
* 理解上图左旋再看代码会容易理解
* 以p节点进行左旋转
* p.right会上升一级
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
// r = p.right
// pp = p.parent
// rl = r.left
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false; // 当p没有上一个节点时,r设置为root
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
/** 右旋方法
* 理解上图右旋再看代码会容易理解
* 以p节点进行右旋转
* p.left会上升一级
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
// l = p.left
// pp = p.parent
// lr = l.right
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;// 当p没有上一个节点时,l设置为root
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
HashMap.treeifyBin(),Node 转为 TreeNode 红黑树
HashMap中由链表转换普通树,再进行平衡转换成红黑树
当单链表Node的数量达到阈值,转换为TreeNode对象
将TreeNode中的结构转化为双向链表,设置前驱和后继
tab[index] 由原来的Node转化为TreeNode
然后在进行树化操作hd.treeify(tab);
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
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(); // 调整table大小
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//p新建节点,循环中的当前节点
TreeNode<K,V> p = replacementTreeNode(e, null); // new TreeNode()
if (tl == null) // 是否头结点
hd = p; // hd = 头结点
else {
p.prev = tl; // 设置前驱
tl.next = p; // 设置后继
}
tl = p; // 配合wile循环,tl为当前节点的上一个节点
} while ((e = e.next) != null); // e = e.next = 当前节点,直到e.next = null
if ((tab[index] = hd) != null)
hd.treeify(tab); // 树化操作
}
}
hd.treeify(tab),TreeNode转为树形结构
由双向链表转换为树型结构
第一个for循环是遍历双向链表,x = 当前节点
root始终为头结点,从头结点开始,进行for循环遍历,直到最后节点=null
else中k,h是当前遍历中x节点(非头结点)的key和hash
当前节点x的上一个节点p,p.hash与x.hash进行比较,值为dir,根据该值判断x应该插入p的左边还是右边,若dir=0 ,则插入左边节点
第二个for循环是从root节点开始遍历树形节点, p通过dir判断是寻找左边还是右边节点,直到找到空位子插入
插入完成,将root进行红黑树平衡后并赋值root,x = 插入的节点
简单一句话解释:将双向链表遍历后挨个插入树形节点,树是自平衡红黑树
/**
* 将双向链表 进行 转换树型结构
* root始终等于头结点
* 第一个for循环是遍历双向链表,x = 当前节点
* 第二个for循环是从root节点开始遍历树形节点,通过dir判断是寻找左边还是右边节点
* 简单一句话解释:将双向链表遍历后挨个插入树形节点,树是自平衡红黑树
* Forms tree of the nodes linked from this node.
* @return root of tree
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null; // root始终 = 根节点
// 遍历循环双向链表(this当前刚转换双向链表)
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;// 双向链表的下一个节点
x.left = x.right = null; // 左右节点清空
if (root == null) { // 判断是否头结点
x.parent = null;
x.red = false; // 头结点为黑色
root = x;
}
else { // 头结点非空,执行以下代码
K k = x.key; // 第一轮for循环当前节点的Key
int h = x.hash; // 第一轮for循环当前节点hash
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) { // p 会根据 dir 判断该寻找左还是右节点
int dir, ph; // dir 用于比较大小,大于 = -1,小于 = 1
K pk = p.key; // 上一个节点key
if ((ph = p.hash) > h) // 上一个节点hash 与 当前节点hash比较
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null && // 当ph = h,这里指的是hash相等,则进行类型判断再比较
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp = 上一个节点
// p = dir ? p.left : p.right 一直寻找左节点或者右节点,直到找到空的位置来插入数据
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);
}
TreeNode进行平衡转换(红黑树)
rotateLeft(),进行左旋操作
rotateRight(),进行右旋操作
root = 根节点
x = 插入的节点
进行插入平衡的时候,会出现6种类型的树
其他情况下,或是直接插入不用变色,或是在插入后旋转
/**
* 此处2个条件,才会停止循环。
* 1、xp = x.parent, x节点的没有父节点
* 2、xp是红色 或者 xp没有父节点
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 以下判断一共会有6种树形出现
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
// 第一种树形
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
// 第二种树形
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
// 第三种树形,或是通过第二种树形旋转变成的
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
// 第四种树形
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
// 第五种树形
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
// 第六种树形,或是由第五种树形旋转变成的
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
TreeNode进行平衡转换中出现的6种树形
第一种
第一种树形只需要进行变色操作
然后就是x = xpp,将当前x变为xpp继续遍历
直到整棵树完全符合红黑树规则
第二种
此处未涉及到变色操作
左旋的时候,也仅是将x与xp进行对调
然后根据条件,是否需要进行转换第三种树形结构
第三种
或是满足条件的第二种树形
或是原本就是斜树
判断条件,进行变色之后,再进行右旋操作
第四种
与第一种树形正好相反
第五种
与第二种树形恰恰正好相反
也是未涉及变色操作
第六种
斜树右旋操作
结束语
总结来说嘛,HashMap复杂的地方就在红黑树部分,数组部分其实就是计算hash位置,然后往里塞数据,数组里面存的其实就是链表结构,先计算hash再比较key,通过hash与key来存储链表中的值,在JDK1.8之前,其实就是单链表,为了优化单链表查询效率,引入了红黑树,红黑树的查询方式其实就是二叉查找树,只是红黑树有自平衡机制,以达到较好的查询效率。
最后补一句,由于作者水平有限,若是有错误之处,可以在下方留言,即时改正