HashMap这玩意JAVA开发中常用,但是都是用,至于底层实现原理,都不是什么很清楚,也就只知道什么数组+链表+红黑树!!!那么现在从0开始看。

数组

JDK11源码导读记录(HashMap)_数组


数组+链表

JDK11源码导读记录(HashMap)_数据_02


数组+链表+红黑树

JDK11源码导读记录(HashMap)_java_03


点开我们的HashMap的JDK11源码,先基本上过一遍,在分析JDK11HashMap的时候需要一定的数据进制基础不然会很懵逼的!

基本数据

//初始桶的大小(默认16)必须是2的整数次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//Map的最大容量必须是2的整数次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认负载因子(0.75)
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//链表升级为红黑树的链表长度
static final int TREEIFY_THRESHOLD = 8;

//红黑树降级为链表的链表长度
static final int UNTREEIFY_THRESHOLD = 6;

//扩容时某个链表升级成红黑树的前提是Key-value的对数必须大于64,才能完成升级
//这样可以避免扩容时和链表升级红黑树是产生Hash碰撞冲突
static final int MIN_TREEIFY_CAPACITY = 64;

基本属性

//存放Hash的数组
transient Node<K,V>[] table;

//存放Key-value的数组节点
transient Set<Map.Entry<K,V>> entrySet;

//HashMap的元素数量
transient int size;

//HashMap扩容时的计数器,防止在迭代的时候产生插入、删除破坏原有HashMap的结构
transient int modCount;

//该字段用于判断核实扩容(capacity * load factor)
//默认(16*0.75)得到扩容的阈值,达到这个阈值开始扩容,调用resize()扩容方法
int threshold;

//负载因子
final float loadFactor;

构造方法
在HashMap的源码中提供了3个构造方法,一般大家用的最多的也就是无参构造HashMap(),

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 让负载因子等于默认的0.75
}
public HashMap(int initialCapacity) {//初始化桶(容量的大小)
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

这个构造方法还要调用一个内部方法

public HashMap(int initialCapacity, float loadFactor) {//初始化桶、负载因子
if (initialCapacity < 0)//初始化桶数据合法检查
throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//初始化桶的最大容量不能大于1 << 30;且为2的整数次方
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//负载因子数据合法性检查
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//
}

为给定的目标容量返回最接近2的整数次幂的值。

static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

看完这三个构造方法和一个内部方法后,不难发现,HashMap初始化的时候并没有创建数组容器,而是干了一件数据检查的事情,对没错!确实这里没有初始化数组,真的只是检查初始化桶的大小是否满足1 << 30;且为2的整数次方,负载因子是否合法

对外提供方法
看到这个阶段,会发现HashMap的源码中还有很私有方法,这些私有方法看起来很懵逼,而且没有头绪,那么我们就结合对外暴露的方法来看看到底是怎么回事
1.put方法(HashMap创建数组的时候其实是在put的时候创建的)

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

hash就是得到key对应的hash值

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//key.hashCode()得到key的hash值
//h=hash,换算成2进制数据在进行^运算(起到位扰动的效果,减少hash碰撞,使数据散列分布(极大可能均匀分布)在数组上)
//例如h=1101 0001 0101 0101 1111-->换算成32位高位补齐(不足32位,左边高位补0,直到满足32位)
//补齐h=0000 0000 0000 1101 0001 0101 0101 1111(高位补齐后的32位数据)
//(h >>> 16)往低位移动16位↓↓↓下,也就是往有移动16位然后高位补0,超过32位溢出部分舍弃
//右移16位后的数据0000 0000 0000 0000 0000 0000 0000 1101
//得到补齐后的32位数据和右移16位的数据
//1.0000 0000 0000 1101 0001 0101 0101 1111(32位高位补齐数据)
//2.0000 0000 0000 0000 0000 0000 0000 1101(32位右移后的数据)
//将32位高位补齐数据和32位右移后的数据的进行^(异或运算)1异或运算2,上下数据对比
//异或运算:00=0,11=0,10=1,01=1;
//那么1^2运算后的数据就是↓↓↓根据上面分析的数据得到
//1^2=0000 0000 0000 1101 0001 0101 0101 0010
}
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;
//i = (n - 1) & hash]计算tab数组的索引
if ((p = tab[i = (n - 1) & hash]) == null)//判断当前数组的index位置没有存储元素
tab[i] = newNode(hash, key, value, null);//创建新的节点放到数组中去
else {//如果产生Hash碰撞,也就是同一个坑位中已经有值了
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 (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) {//链表的尾节点是null
p.next = newNode(hash, key, value, null);//创建节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//判断是否大于8,大于转成红黑树
treeifyBin(tab, hash);//转成红黑树
break;
}
//判断链表中是否有重复值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { //判断是否有重复key,替换旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//迭代器,failfast机制;
//在HashMap使用迭代器时,在remove、put元素的时候可能会导致迭代器中的内容数据破坏,
//比如,HashMap的长度是5,当在remove、put的过程当中,HashMap的元素可能会变成4个或者7个,
//那么这个时候迭代去里面就会抛出异常
++modCount;
if (++size > threshold)//判断一下当前的HashMap容量是否已经达到扩容的阈值
resize();//进行扩容
afterNodeInsertion(evict);
return null;
}

resize()-Map扩容

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//初始化table数组的值为null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//Node数组已经初始化,扩容
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) // 初始容量设置为阈值,初始化容量已经占有了
newCap = oldThr;
else {//初始阈值为零表示使用默认值,进行数组初始化
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;
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;
}

HashMap底层数据结构
JDK<=1.7:数组+链表
JDK>=1.8:数组+链表+红黑树

JDK1.7的resize()多线程扩容时会链表产生死循环俗称JDK1.7HashMap死循环,是链尾指向链头导致指针循环在链表中运行
JDK1.8的HashMap完善了resize(),put()方法,底层采用数组+链表+红黑树,当Hash产生碰撞后生成链表,当链表长度达到8的时候由链表转换成红黑树,当红黑树的节点小于等于6的时候,转换成链表,resize()方法在1.8版本修改了扩容机制,避免多线程扩容的时候产生链表死循环,这里扩容就是根据负载因子算出要扩展的容量,然后创建新容量的HashMap,在将旧的HashMap中的值放入新的HashMap,这里注意(链表从头插入值(减少时间复杂度O(1),如果要从尾部插值的话那么就要经过整个链表,时间复杂度就是O(N))),但是在导入旧的链表时,链表顺序就反过来了,虽然JDK1.8修复了HashMap多线程扩容链表死循环的问题,但是官方不建议多线程情况下使用HashMap

  1. 数组的时间复杂度:O(1)
  2. 链表的时间复杂度:O(N)
  3. 红黑树时间复杂度:O(logn)
    HashMap的源码就分析到这里了,想必看完对HashMap多少不止停留在数组+链表+红黑树,多少知道一些底层原理。