您的位置:首页 > 其它

HashMap源码分析(学HashMap源码看这一篇就够了)

2020-05-23 21:19 423 查看

写在前面:这篇博客主要是针对HashMap的源码分析,读源码最忌心浮气躁,希望大家能静下心来一句一句阅读代码和注释,相信你一定有收获

HashMap简介

HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。在jdk1.8以前HashMap主要基于数组加链表来实现,而jdk1.8以后HashMap通过数组加链表加红黑树来实现

HashMap的底层结构和核心理论

HashMap的底层结构(图片来源于网络)

HashMap通过hash函数将对应的key转化成固定长度的值,来充当下标映射到数组对应的位置
Hash的特点:

  1. 通过hash值不能反向推导出原始的数据的值
  2. 相同的元素会得到相同的hash值

路由寻址算法:hash&size-1(hash为hash码,size为数组长度)
路由寻址算法找到的即为key在数组中对应的下标值,即得到对应的存放位置

哈希冲突:hash的原理是将输入空间的值映射到hash空间内,由于hash的空间远小于输入的空间,所以可能导致不同的输入映射到相同的hash空间处,即哈希冲突。这也是为什么HashMap()要使用数组加链表加红黑树的结构就是为了解决发生hash冲突的情况。

加载因子(loadFactor):用来计算扩容阈值,公式为:thresold=loadFactor*size(size为数组长度)
loadFactor默认为0.75f
扩容阈值(thresold):容量达到扩容阈值进行扩容
HashMap源码分析(此次只选取部分重要代码分析)
变量:

//序列号
private static final long serialVersionUID = 362498820763181265L;
//    默认初始容量为16,1 << 4表示1左移4位
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//    HashMap数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//    默认构造因子
/*    DEFAULT_LOAD_FACTOR用来计算HashMap的扩容阈值,即
*     即什么时候对HashMap进行扩容 计算公式为:thresold=loadFactor*size(size为HashMap数组大小)
*     当数组容量大于16时扩容时新的阈值为旧阈值的两倍,而不计算
*
* */
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;
int threshold;//扩容阈值,容量到达阈值时进行扩容`

构造方法
HashMap提供了四个构造方法 ,其中3个有参构造,1个无参构造

//构造方法1,用来构造指定的初始容量和加载因子的HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//容量要大于0
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
//如果容量大于默认最大容量则设为默认最大容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//通过给定的initialCapacity来计算初始容量,HashMap要
//求数组容量为2的n次方的格式,所以需要对你输入的数进行格式化
//tableSizeFor的作用就是返回大于或等于且最接近initialCapacity
//的2的n次幂的格式的数
this.threshold = tableSizeFor(initialCapacity);
}

// tableSizeFor源码
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;//n=n|n>>>1,n等于n与n右移一位进行位或(有1为1)
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//位或执行的结果是让输入的参数最高位的1后面全变成1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//最后再加一即得到一个大于等于输入数
//(最接近输入数)的2的n次幂的数
}
构造方法2
public HashMap(int initialCapacity) {//用来构造指定的初始容量的HashMap
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
构造方法3
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
构造方法4
public HashMap(Map<? extends K, ? extends V> m) {
//用来构造HashMap,将Map的数据赋值给它
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

put方法

//    调用putVal方法进行存放数据操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash方法(扰动函数)
//作用:让key的高16位也参与路由寻址运算,减少哈希碰撞
//路由寻址运算为hash&size-1(size为数组长度)
//一开始数组长度不大,故在进行&运算时并不能让hash码的所有位都
//参与运算(只有低位的能参与运算),所以让hash码的高位与低位进行异或
//运算可以让hash的高位也可以参与和size-1的&运算
static final int hash(Object key) {
int h;
//key.hashCode()调用本地方法获取hash码
//(h = key.hashCode()) ^ (h >>> 16) hashCode的低16位与高16位进行异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//putVal方法
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)//将table赋值给tab并判断是否为空(判断数组是否已初始化)
//HashMap第一次初始化不是在创建对象时,而是在调用put方法时
n = (tab = resize()).length;//数组未初始化,调用扩容函数resize进行初始化
//        数组已初始化
if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash路由寻址,找到该key在数组对应的下标并将对应位置的数据赋值给p
//            p为null表示在数组的该位置还没有存放数据,故直接存放进去就好了
tab[i] = newNode(hash, key, value, null);
//        数组已初始化且通过路由寻址找到的数组对应位置已有数据存放(发生hash冲突)
else {
Node<K,V> e; //存放结点
K k;
//            判断在数组该位置上的元素是不是与存放的元素key值是否相等
//            注:hash值相等,两个数据不一定相等,但两个数据相等,hash值一定相等
//            ***
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//相等把p赋值给e
//数组该位置上的元素与存放的元素key值不等
else if (p instanceof TreeNode)//判断是不是红黑树
//                红黑树等专门写一篇来记录
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//说明数组首元素与要存入元素key值不等,且后面结构不是红黑树(即为链表)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//到达链表尾部直接存放在链表尾
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 判断结点是否到达阈值,到达了要转化为红黑树
treeifyBin(tab, hash);
break;
}
//                   *** 判断链表中的结点与要存入元素key值是否相等,e此时存放对与要存入元素key相等的结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;// p = e与e = p.next组合用于遍历链表
}
}
//            ***处发生时执行的代码
if (e != null) { // 该结点key值与要存入元素相等
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//onlyIfAbsent:putValue函数参数,为true则替换key相同元素
e.value = value;
afterNodeAccess(e);//访问后回调
return oldValue;
}
}
++modCount;//表示散列表结构被修改的次数 替换不算
if (++size > threshold)//实际大小大于阈值则进行扩容
resize();
afterNodeInsertion(evict);//插入后回调
return null;
}

接下来是最核心的resize方法
首先解释一下为什么要进行扩容,HashMap进行扩容最重要的原因是数据变多时,hash冲突频率也会变高,此时会影响HashMap函数的查找效率(时间复杂度会从O(1)变成O(n)),所以需要扩容

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//存放数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//当前数组容量,oldTab为null说明是第一次存放数据(初始化)
int oldThr = threshold;//扩容阈值
int newCap, newThr = 0;
if (oldCap > 0) {//数组已经初始化了
if (oldCap >= MAXIMUM_CAPACITY) {//数组长度达到最大值了,此时不再进行扩容
threshold = Integer.MAX_VALUE;//将扩容阈值设置为int的最大值(很难达到)
return oldTab;
}
//            扩容oldCap << 1,数组容量翻一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//
newThr = oldThr << 1; // 当容量大于16时不再用加载因子计算阈值,而是直接将当前阈值翻倍
}
//       oldCap==0的情况
else if (oldThr > 0) // 数组还未初始化了   调用构造方法时指定了初始容量大小
newCap = oldThr;
else {               // 数组还未初始化了   调用构造方法时没有初始容量大小
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//数组容量小于16或调用构造方法时指定容量,使用公式计算阈值
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;
//        复制算法
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;//当前node结点
if ((e = oldTab[j]) != null) {//说明当前桶位有数据,给e赋值
oldTab[j] = null;//把原数组对应位置置空,方便JVM进行GC时回收
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 { // 链表
/*
* 扩容后数组长度翻一倍,说明在用路由寻址函数时size-1比原来的数组多了一位
* 此时hash不变,则寻址有两种结果
*   1、hash与size多一位对应处数为0,则数据存放在新数组处的位置与原数组相等(低位j结点)
*   2、hash与size多一位对应处数为1,则数据存放在新数组处为原数组位置加上旧数组长度(高位结点)
*   如原来数组长度为16(size-1=1111),扩容后为32(size-1=11111)
*   此时01111与11111在旧数组经过路由寻址后得到的下标值相同
*   而在新数组中的寻址结果却不同
*   所以在复制时需要对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) {//判断hash与新数组最高位&的那一位是0还是1
if (loTail == null)//e为第一个元素,插入新数组
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;
}

结尾

关于HashMap的常用方法源码分析到这里就结束了,如果有什么错误希望各位大佬能帮忙指正,如果有什么问题也可以评论大家一起讨论。如果觉得这篇文章对你有帮助的话希望能帮我点个赞

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: