HashMap源码浅析
某次恰饭的时候,问头儿,他进来的时候,被面试了哪些内容。从java基础,到框架原理顺着一大堆东西就说出来了。一个HashMap,也可以问出花来。本次就以HashMap开个头,来探一探源码的实现。
我们都知道Map是一种由多组键值对集合在一起的结构,其中key不可以重复,value值可以。HashMap作为Map接口的常用实现,在java8之前底层实现是数组+链表,在java8之后使用数组+链表+红黑树来实现。
本文主要以java8为主进行展开,主要讲解了底层结构、put、get、扩容的相关实现机制和源码分析。有关红黑树以及线程的部分,本次暂未涉及。
1 底层结构
hashMap总体上使用数组+链表来实现。
在java中hashcode是一个int值,实际使用当中,hashCode往往集中出现在某个区域,此时为了解决哈希冲突,引入链表对value进行存储,又因为链表可能很长,为了提高效率,在java8中引入了红黑树。
在java7及之前,主干是一个Entry数组,Entry<K,V>是一个实现了Map.Entry<K,V>接口的静态内部类;在java8中,数组的类型改为Node<K,V>[] table;,同样是实现了Map.Entry<K,V>接口的静态内部类。
不同的是,Node保留了链表的特点同时(具有指向下一个结点的指针),还有一个子类TreeNode<K,V>,可以实现树结构。
//数组
transient Node<K,V>[] table;
//数组类型
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) { ... }
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
//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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
}
这里的继承关系如下图所示:
2 存入数据
put(K,V)方法源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
主要进行的操作:①获取key的hashCode,②调用putVal()方法进行存值;
2.1 hash的获取和下标计算
hash()的源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里在获取到key的hashCode之后,进行了哈希分散,将32位的哈希值的高16位异或低16位,使得在计算下标的时候,即便length很小,高低位都参与运算。
这里值得注意的是,在根据hashCode计算下标时,取模运算通过与运算实现,提高效率。
(length - 1) & hash 等价于 hash%length
2.2 putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
hash
代表key的hash值;
onlyIfAbsent
代表是否取代已存在的值;
evict
为继承预留的布尔值;
根据源码,流程图如下:
可见在put最开始和最后,会进行扩容的检查;并且通过key的比较和类型区分出各种情况,找到key进行value覆盖,或者进行链表尾插,或者红黑树插入。
关于红黑树的具体属性,代码操作,本文不再阐述,之后专门写一下。
源码如下:
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)
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;
}
3 取出数据
有了对put()的分析,get()就简单很多。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
在get中,同样只做两步操作,获得key的hashCode,调用getNode()。
final Node<K,V> getNode(int hash, Object key) {}
参数分别是key的hashCode和key本身。
和put相比,get的开始和最后不再有扩容的判断,直接根据hashCode计算下标位置,然后分情况讨论:
从图中可以看出来,同样是分为下标指向元素为空、链表、红黑树的情况。
源码如下:
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) {
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;
}
4 扩容
4.1 相关常量&变量
当整个map当中的键值对数量 > 容量*负载因子的时候,进行扩容
梳理扩容,首先来了解一下HashMap的几个常量:
/*默认初始容量,必须是2的幂*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/*哈希表最大容量,在构造函数指定容量的时候,用于比较*/
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;
其中MIN_TREEIFY_CAPACITY
指的是:在转变成树之前,还会有一次判断,只有键值对数量大于该值才会发生转换,数量比较少就先扩容。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
此外还有一些成员变量:
int threshold; // 所能容纳的key-value对极限 = length * Load factor
final float loadFactor; // 负载因子
int modCount; // 记录HashMap内部结构发生变化的次数
int size; // 实际存在的键值对数量
其中,modCount
指的是结构的变化,值的覆盖不算,主要用于迭代的快速失败,(那么快速失败也是一个点,先留下)。
4.1 扩容机制
在java7及之前, 将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
因为使用的是2次幂的扩展,每次数组大小翻倍,所以元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
上文已经说过,hashCode转化为下标,采用的是 (n-1)&hashCode
故有n=16时 两个不同的key的hashCode,计算出同样的下标:
进行扩容之后(n=32):
不难看出,此时下标的结果不再相同,一个是12,一个是16+12,对应二进制只是在增加的高位不同。
故新增高位为0的时候,位置不变;为 1则是oldIndex+OldCap 。
java8利用了这个规律进行扩容。
源码如下(增加了注释批注):
final Node<K,V>[] resize() {
/*初始化一些变量*/
Node<K,V>[] oldTab = table;
/*旧表长*/
int oldCap = (oldTab == null) ? 0 : oldTab.length;
/*旧阈值 (= length * Load factor)*/
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
/*旧表长大于哈希表最大容量---少见的特殊情况,直接将map阈值置为Integer.MAX_VALUE*/
/*超过最大值就不再扩充了*/
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
}
/*表之前大小为0或为null,但是阈值>0,初始化容量为阈值 */
else if (oldThr > 0) // 初始化容量为阈值
newCap = oldThr;
else { // 零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/*计算新的resize上限*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
/*初始化扩充后的table数组*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
/*把每个bucket都移动到新的buckets中*/
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
/*链表重hash,将链表上的数据,分两类连接起来,最后再指向对应的表上*/
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 {
/*oldIndex+OldCap*/
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
/*原索引放到bucket里*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
/*oldIndex+OldCap放到bucket里*/
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5 equals()和hashCode()
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
以上说法已经是一种共识,以hashMap为例,理想情况下,只有hashCode一致和equals比对一致,才会认为是相同的对象。
hashMap会首先比较hashCode值,如果不等的话,会直接认为是两个对象,不再使用equals()进行比较。
如果只修改equals()方法,两个对象在业务上相等,hashCode不一致,则都会顺利存入map当中,造成数据的混乱。
本文地址:https://blog.csdn.net/netxiaozhe/article/details/109567865
上一篇: 清明节吃什么传统食物 你知道吗
下一篇: 牛腩的营养价值及功效