0
点赞
收藏
分享

微信扫一扫

HashMap源码解析

书坊尚 2021-09-19 阅读 109
随笔

HashMap原理

1.HashMap存储结构

从结构来讲,HashMap是有数组,链表,红黑树(jdk1.8之后加入)实现的,如下图所示

引入红黑树是因为他查找,插入,删除的平均时间复杂度为O(log(n))。这是因为当产生hash碰撞的时候,数据会挂载(尾插),形成链表,链表空间上不连续,逻辑上连续,增删元素块,只需要采用节点间引用,时间复杂度为O(1),查询慢,需要遍历查找,时间复杂度为O(n);

2.源码分析

2.1构造方法
// initialCapacity 初始容量  默认16
// loadFactor 加载因子 默认 0.75
// threshold 阈值 = initialCapacity*loadFactor
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 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;
this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

HashMap构造方法一共重载了四个,其中初始化三个参数

  • initialCapacity:初始容量,默认为16,HashMap底层由数组+链表(或红黑树)实现,但是还是从数组开始,所以当储存的数据越来越多时,就必须进行扩容操作,如果在知道需要存储数据大小的情况下,指定初始容量可以避免不必要的扩容操作,提升效率。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
  • loadFactor:加载因子(默认0.75),当负载因子较大的时,去给table数组扩容的可能性会少,所以相对占用内存就会较少(空间上较少),但是每条entry链上的元素相对较多,查询的时间就会增长(时间上较多)。反之就是,负载因子较小的时候,给table数组扩容的可能性就会变大,那么占用内存空间就会较大,但是entry链上的元素就会较少,查询的时间也会减少。所以才有了负载因子是时间和空间上一种折中的说法。所以设置负载因子的时候要考虑自己追求的事时间上的少还是空间上的少(一般情况下不需要设置,系统给定的默认值就是比较合适了)。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • threshold :阈值,hashMap所能容纳最大键值对的数量,如果超过则需要扩容,计算方式 ,构造方法中通过#tableSizeFor(initiaCapacity)方法进行了赋值,主要原因是在构造方法中,数组table并没有进行初始化,而是在put方法中进行初始化的,同时在put方法中也会对threshold进行重新赋值。
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 找到大于或等于cap的最小2的幂
* 不考虑最大容量的情况下,返回cap且最近接2的整数次幂
*/

static final int tableSizeFor(int cap) {
int n = cap - 1; // 防止cap已经是2的整数次幂
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

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

static final int TREEIFY_THRESHOLD = 8;

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尚未初始化,则此处进行初始化数组,并赋值初始容量,重新计算阈值,默认长度为16
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存在哪个桶中
if ((p = tab[i = (n - 1) & hash]) == null)
// 通过hash找到下标,如果hash置顶的位置为空,则直接将该数据存放进去
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))))
// 如果需要插入的key与当前hash值指定下标的key一样,现将第一个元素p赋值给e
e = p;
// 如果节点为红黑树节点
else if (p instanceof TreeNode)
// 放入到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 其余情况则为链表节点
else {
// 遍历当前链表,在链表的最末端插入节点(jdk1.7采用头插法,容易造成死循环)
for (int binCount = 0; ; ++binCount) {
// p.next 为空则代表链表尾端
if ((e = p.next) == null) {
// 在链表尾部插入节点
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 扩容阈值8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果达到阈值,则转为红黑树
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中元素的key与插入元素key值是否想的
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 如果相等,掉出循环
break;
// 遍历桶中的列表,与前面e = p.next 组合,可以遍历链表
p = e;
}
}
// 如果e有记录,则表示桶中找到与元素key值,hash值与插入元素相等的节点,说明上面的值以及存在于当前的hashmap中,更新指定指定 // 键值对
if (e != null) { // existing mapping for key
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent true 不改变已经存在的值
// onlyIfAbsent false
if (!onlyIfAbsent || oldValue == null)
// onlyIfAbsent 为false,或者新插入的value为空,用新值替换旧值
e.value = value;
// 回调,将元素插入到链表的最后
afterNodeAccess(e);
// 返回旧值
// map.put("haha","hehe");
// map.put("haha","heiheihei");
// return hehe
return oldValue;
}
}
// 记录内部结构发生变化的次数
++modCount;
// 每当put一个元素,当实际大小,小于大于阈值的时候,进行扩容
if (++size > threshold)
// 扩容
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}

具体过程如下图

2.3resize方法
        final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// table 已经初始化,且容量>0
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果旧的容量已经达到最大值2^30,则不再扩容,阈值直接设置为最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果旧数组*2小于最大容量2^30 并且 旧数组的常量大于等于初始容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 阈值扩容的大小为当前的2倍
newThr = oldThr << 1; // double threshold
}
// 旧阈值>0,
// 说明使用的构造方法为HashMap(int initialCapity,int loadFactory),该方法中,
// this.threshold=tableSizeFor(initialCapity),返回的容量为2的n次幂
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 阈值为初始化0,oldCap为空,及创建数组是为无参构造方法,调用resize()初始化默认值,
// 将新的初始化长度设置为16,阈值设置为16*0.75=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值为0,根据负载因子设置初始化的值
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"})
// 创建一个长度为newCap的Node数组
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) {
//1.将指定下标数据置空,
oldTab[j] = null;
// 2.指定下标只有一个元素
if (e.next == null)
// 重新计算hash值,确定元素的位置
newTab[e.hash & (newCap - 1)] = e;
// 红黑树 treenode数据结构
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;
// 如果hash值为0,元素在数组中的位置未发生改变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 不为0,元素在扩容数组中的位置发生改变,新的下标为原索引+oldCap
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;
}

注意

2.4get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1.根据hash算法找出对应位置的第一个数据,如果key相等,则直接返回
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;
}

说明

2.4.1 hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

说明:这段代码叫做扰动函数,也是HashMap中的hash运算。

2.4.2 取模运算 (n-1) & hash

3.总结

  1. HashMap底层结构在1.7之前是数组+链表组成的,1.8之后加入了红黑树;链表的长度小于8的时候,发生hash冲突的时候会增加链表的长度,当链表长度大于8的时候,会先判断数组的容量,如果容量小于64则先扩容(原因是数组长度越小,越容易发生碰撞,因此当容量小的时候,首先考虑的是扩容),如果容量大于64,则将链表转换为红黑树,提升效率。
  2. hashmap的容量为2的n次幂,无论在初始化的时候传入的容量的值为多少,都会转换为2的n次幂,这样做的原因是为了在取模运算的时候可以用&而不是用%,可以极大的提升效率,同时也降低了hash冲突。
  3. HashMap是非线程安全的,在多线程的情况下会存在异常(如形成闭环),1.8的时候已修复闭环问题,但仍是线程非现场安全的。可以使用hashtable和ConcurrentHashMap代替。

4.问题

1.为什么主数组的长度要为2的n次幂,如何保证?

2.为什么桶中节点个数超过8个才会转成红黑树?

举报

相关推荐

0 条评论