基本概念:
- jdk1.8的HashMap底层的数据结构是,数组+链表+红黑树,当我们存储元素的时候,如果存在hash冲突,链表的深度会不断加深,同时集合的容量会不断增加,当到达一个需要转化的点之后(如下),链表会转化成红黑树。
- 链表的深度达到8
- 集合的容量达到64
- 当我们删除元素的时候,如果红黑树中元素的数量减小到6会转换成链表结构
成员变量
/**
* 默认的初始化容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大的容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的扩容因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当链表深度到达8的时候转化成红黑树的点
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当红黑树中元素节点的值小于6的时候转换成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当整个hash表中的元素到达64的时候才会进行树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 当前hash表的数组 也就是hash桶
*/
transient Node<K, V>[] table;
/**
* 作为entrySet的缓存
*/
transient Set<Entry<K, V>> entrySet;
/**
* 元素的数量
*/
transient int size;
/**
* 迭代的次数,用于快速失败策略
*/
transient int modCount;
/**
* 当前map的容量到达多少的时候进行扩容处理
* threshold = capacity * loadFactor(下面那个参数) 扩容门槛
*/
int threshold;
/**
* 扩容因子 用于计算容器扩容的点 默认是0.75
*/
final float loadFactor;
- 需要重点关注的几个成员变量是
/**
* 默认的扩容因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当链表深度到达8的时候转化成红黑树的点
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当红黑树中元素节点的值小于6的时候转换成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当整个hash表中的元素到达64的时候才会进行树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 当前map的容量到达多少的时候进行扩容处理
* threshold = capacity * loadFactor(下面那个参数) 扩容门槛
*/
int threshold;
/**
* 扩容因子 用于计算容器扩容的点 默认是0.75
*/
final float loadFactor;
两个底层的数据结构
- 链表
/**
* 代表的是链表结构
*/
static class Node<K, V> implements Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
....................
}
- 红黑树
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
TreeNode<K, V> parent;
TreeNode<K, V> left;
TreeNode<K, V> right;
TreeNode<K, V> prev;
boolean red;
.......
}
构造函数
- 无参构造函数
/**
* 空参构造方法,全部使用默认值。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 传入初始化容量的构造函数
/**
* 调用HashMap(int initialCapacity, float loadFactor)构造方法,传入默认装载因子。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);《1》
}
- 《1》调用了this(initialCapacity, DEFAULT_LOAD_FACTOR);方法
/**
* 判断传入的初始容量和装载因子是否合法,
* 并计算扩容门槛,扩容门槛为传入的初始容量往上取最近的2的n次方。
*/
public HashMap(int initialCapacity, float loadFactor) {
// 检查传入的初始容量是否合法
if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 这句话表明hashmap的最大值就是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 检查装载因子是否合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// <1>设置装载因子
this.loadFactor = loadFactor; // 0.75
// <2>计算扩容门槛 扩容门槛为传入的初始容量往上取最近的2的n次方。
this.threshold = tableSizeFor(initialCapacity);
}
- <1>自定义容量的构造函数设置的默认装载因子(用于扩容门槛计算)
- <2>扩容门槛为当前容量向上取整最近的2的n次方
- <3>留一个疑问:这个扩容门槛都比当前容量大了????mmpd的 这放数据放多了咋玩? 还好没创建集合呢 。 后面真正创建集合的时候会有答案。
put添加元素
/**
添加元素的入口。
*/
public V put(K key, V value) {
// 《2》 putVal 真正放置元素的方法
return putVal(
hash(key), // 《1》调用hash(key)计算出key的hash值
key,
value,
false,
true);
}
- 《1》调用hash(key)计算出key的hash值
/**
* 对key进行hash运算
* 为了更加的提高他的分散值 对他进行低16位和高16位的异或操作
*/
static final int hash(Object key) {
int h;
// 低16位和高16位进行异或运算 提高利用率
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 为了让元素进入到不同的链表中,增加元素的分散比率,这里对key的hashCode值做了一个高16位和低16位的异或操作
- 这里还有一个疑问hashcode取出来之后这么大,怎么就放到有数的这几个hash桶中的呢? 大家都能想到办法就是取余。真是这么搞的嘛?
- 《2》 putVal 真正放置元素的方法
/**
* 存放元素的值
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 生明hash表
Node<K, V>[] tab;
// 声明单个hash元素
Node<K, V> p;
int n, //数组的长度
i; //数组的位置
// 如果当前hash桶的长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//<1>调用resize方法进行初始化
n = (tab = resize()).length;
//<2>算出当前元素在hash表中的位置
/*如果当前位置一个元素都没有*/
if ((p = tab[i = (n - 1) & hash]) == null)
// 新建一个节点放到hash桶中
tab[i] = newNode(hash, key, value, null);
// 如果当前桶中已经存在元素了
else {
Node<K, V> e;
K k;
// 如果桶中第一个元素的key与待插入元素的key相同 hash表的第一个位置
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
//保存到e中用于后续修改value值
e = p;
// 如果第一个元素是树节点
else if (p instanceof TreeNode)
// 调用树节点的putTreeVal插入元素
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
// 不是树节点 而且不在hash桶的第一个位置,那就是在当前hash桶的元素里面
//进去找(循环遍历找)
else {
for (int binCount = 0; ; ++binCount) {
// 如果链表遍历完了都没有找到相同key的元素,
if ((e = p.next) == null) {
// 说明该key对应的元素不存在,则在链表最后插入一个新节点
p.next = newNode(hash, key, value, null);
// 如果插入新节点后链表长度大于8,则判断是否需要树化,
//因为第一个元素没有加到binCount中,所以这里-1
if (binCount >= TREEIFY_THRESHOLD - 1)
//<3>进行树化
treeifyBin(tab, hash);
break;
}
// 如果待插入的key在链表中找到了,则退出循环
// 且当前p保存到e中用于后续修改value值 也就是替换
if (e.hash == hash && ((k = e.key) == key
|| (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到了对应key的元素
if (e != null) {
// 记录下旧的值
V oldValue = e.value;
// 判断是否需要替换旧值
if (!onlyIfAbsent || oldValue == null)
// 替换旧值为新的值
e.value = value;
// 在节点被访问后做点什么事,在LinkedHashMap中用到 实现为null
afterNodeAccess(e);
// 替换完毕之后返回旧的值
return oldValue;
}
}
// 增加修改次数
++modCount;
// 给当前size++ 并且判断当前容量是否超值 是否需要扩容
if (++size > threshold)
// 进行扩容处理
resize();
// 在节点插入后做点什么事,在LinkedHashMap中用到
afterNodeInsertion(evict);
//没有找到元素返回为null 只有在put新的值的时候才会返回为null
return null;
}
- 总结流程:
- (1)计算key的hash值;
- (2)如果桶(数组)数量为0,则初始化桶;
- (3)如果key所在的桶没有元素,则直接插入(如果是初始化的那就是没值咯,直接放呗);
- (4)如果key所在的桶中的第一个元素的key与待插入的key相同,说明找到了元素,转后续流程(9)处理;
- (5)如果第一个元素是树节点,则调用树节点的putTreeVal()寻找元素或插入树节点;
- (6)如果不是以上三种情况,则遍历桶对应的链表查找key是否存在于链表中;
- (7)如果找到了对应key的元素,则转后续流程(9)处理;
- (8)如果没找到对应key的元素,则在链表最后插入一个新节点并判断是否需要树化;
- (9)如果找到了对应key的元素,则判断是否需要替换旧值,并直接返回旧值;
- (10)如果插入了元素,则数量加1并判断是否需要扩容;
- <1>调用resize方法进行初始化,留在后面进行专门讲解
- <2>算出当前元素在hash表中的位置
if ((p = tab[i = (n - 1) & hash]) == null)中的(n - 1) & hash是真正计算存储到数组的位置
假设我们的数组的容量n是16而且要存的是20 二进制(00010100)
16-1 等于15 二进制( 01111)
16 00010100
15 01111
结果 00100最后的结果就是 00100 也就是4 这不是和取模运算一样嘛? 不过&运算比%运算效率高哦。
其实,(n - 1) & hash 就是取当前hash值和数组容量的余数操作之不说是算法不一样而已。
这里要提一点的是(有没有发现hashMap的容量都是2的次方 发现了新大陆 居然和这个hash值的运算合起来了,写hashMap这个大佬真是爸爸)
- <3>进行树化 treeifyBin(tab, hash); 留在后面进行专门讲解
来了来了重头戏(黑帮老大级别)resize方法(扩容+初始化)
final Node<K, V>[] resize() {
// 记录下旧的hash桶
Node<K, V>[] oldTab = table;
// 记录下原来hash桶的是数量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 记录下原容器需要扩容的点 这个是采用非默认构造函数之后创建出来的
int oldThr = threshold;
// 声明新的hash桶的长度 和 新容器需要进行扩容的点
int newCap, newThr = 0;
// 《1》这个是扩容
// 如果当前hash桶中有值则不是进行初始化
if (oldCap > 0) {
// 如果当前hash桶的数量大于等于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 则扩容因子为int类型的最大整数 不在进行扩容
threshold = Integer.MAX_VALUE;
// 不进行扩容 返回旧的hash桶
return oldTab;
// 如果旧容量的两倍小于最大容量并且旧容量大于默认初始容量(16),则容量扩大为两部,扩容门槛也扩大为两倍
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 这里oldThr 最少也是2
// 使用非默认构造方法创建的map,第一次插入元素会走到这里
//《2》这个是初始化(非默认构造方法的哦)
} else if (oldThr > 0)
// 如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛
newCap = oldThr; // 这里newThr可能为0 就是这里了 因为这里没设置值。
//《3》这个是初始化(默认构造方法的哦)
// 调用默认构造方法创建的map,第一次插入元素会走到这里
else {
newCap = DEFAULT_INITIAL_CAPACITY; //声明新的容量
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 扩容门槛= 默认的装载因子 * 默认的初始容量为16 这里newThr就不可能为0
}
// 《4》 这个是计算扩容因子(非默认构造方法才会走的哦)
// 这里就是hashmap采用非默认构造参数走的方法
if (newThr == 0) {
// 这里面的newCap = 扩容门槛 前面有赋值进来
float ft = (float) newCap * loadFactor; // ft = 扩容门槛*装载因子(扩容门槛是采用非默认构造函数之后创建出来的 装载因子是采用非默认构造函数传递进来的)
// 如果旧的扩容门槛小于最大的容量,而且 新的扩容门槛小于最大容量 则把新的扩容门槛赋值给当前扩容门槛 否则则以后都不进行扩容
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
// 把旧的扩容门槛赋值给新的扩容门槛
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
// 扩容之后的hash数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab; // 把新数组赋值给当前成员变量 hash桶
// 这里就是真的走的扩容的逻辑 初始化不走
if (oldTab != null) { // 如果旧的数组不等于null
// 循环遍历数组中的值
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
// 把值取出来到e中 并且判断当前是不是null 如果是null 那么就代表当前链表根本就没有值在这
if ((e = oldTab[j]) != null) {
// 如果不为null 把当前置空 便于垃圾回收机制回收
oldTab[j] = null;
// 如果e的下一个元素为null 代表的就是当前只有一个值在这 直接原算法 因为数组长度扩大了 所以要重新计算位置
if (e.next == null)
//则把值放到新的数组的位置
newTab[e.hash & (newCap - 1)] = e;
//如果下面是红黑树 那么就直接把下面split大散掉
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
// 《5》 这里就是有深度的链表需要重新定位在数组中的位置
// 那么就只剩下链表了 这里就是有深度的链表需要重新定位在数组中的位置
else {
// 低位头和尾巴 头记录了第一个元素的位置 也就是hash值 尾用于一个一个往后加元素 加到最后头就代表了整个元素
Node<K, V> loHead = null, loTail = null;
// 高位头和尾巴
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
// 不断遍历链表
do {
next = e.next;
// 《6》 有一个算位置的鬼东西 mmp的
if ((e.hash & oldCap) == 0) {
// 没有尾巴
if (loTail == null)
// 设置头
loHead = e;
else
//设置尾巴的下一个元素
loTail.next = e;
// 设置当前尾巴 当第一次进来的时候是相同的跟头 就代表者头代表了尾向后添加的所有元素(也就是每一个next代表的一串的值)
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 如果存放低位置的节点不是null 那么就放到低位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果存放高位置的节点不是null 那么就放到高位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
- 流程总结
(1)如果使用是默认构造方法,则第一次插入元素时初始化为默认值,容量为16,扩容门槛为12;
else {
newCap = DEFAULT_INITIAL_CAPACITY; //声明新的容量
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
// 扩容门槛= 默认的装载因子 * 默认的初始容量为16
}
- (2)如果使用的是非默认构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方;
1、采用传入初始值的时候的构造函数得到的扩容因子为,初始容量向上取最近的2次方
// 计算扩容门槛 扩容门槛为传入的初始容量往上取最近的2的n次方。
this.threshold = tableSizeFor(initialCapacity);
2、把旧的扩容门槛的值赋值给新容器中的容量(到现在扩容门槛==当前容器容量了)
else if (oldThr > 0)
// 如果旧容量为0且旧扩容门槛大于0,则把旧的扩容门槛的值赋值给新容器中的容量
newCap = oldThr;
// 这里可是没有设置扩容门槛的哦
3、在重新计算一下当前扩容门槛(新容量*装载因子)
// 这里就是hashmap采用非默认构造参数走的方法
if (newThr == 0) {
// 这里面的newCap = 扩容门槛 前面有赋值进来
float ft = (float) newCap * loadFactor;
// ft = 新容量*装载因子
// (扩容门槛是采用非默认构造函数之后创建出来的
//装载因子是采用非默认构造函数传递进来的)
// 如果旧的扩容门槛小于最大的容量,而且 新的扩容门槛小于最大容量
//则把新的扩容门槛赋值给当前扩容门槛 否则则以后都不进行扩容
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft :
Integer.MAX_VALUE);
}
- (3)如果旧容量大于0,则新容量等于旧容量的2倍,但不超过最大容量2的30次方,新扩容门槛为旧扩容门槛的2倍;
// 《1》这个是扩容
// 如果当前hash桶中有值则不是进行初始化
if (oldCap > 0) {
// 如果当前hash桶的数量大于等于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 则扩容因子为int类型的最大整数 不在进行扩容
threshold = Integer.MAX_VALUE;
// 不进行扩容 返回旧的hash桶
return oldTab;
// 如果旧容量的两倍小于最大容量并且旧容量大于默认初始容量(16),
//则容量扩大为两部,扩容门槛也扩大为两倍
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
- (4)创建一个新容量的桶;
// 扩容之后的hash数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab; // 把新数组赋值给当前成员变量 hash桶
- (5)搬移元素,原链表分化成两个链表,低位链表存储在原来桶的位置,高位链表搬移到原来桶的位置加旧容量的位置;
// 《5》 这里就是有深度的链表需要重新定位在数组中的位置
// 那么就只剩下链表了
//这里就是有深度的链表需要重新定位在数组中的位置
else {
// 低位头和尾巴
// 头记录了第一个元素的位置 也就是hash值
// 尾用于一个一个往后加元素 加到最后头就代表了整个元素
Node<K, V> loHead = null, loTail = null;
// 高位头和尾巴
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
// 不断遍历链表
do {
next = e.next;
// 《6》 有一个算位置的鬼东西 mmp的
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);
// 如果存放低位置的节点不是null 那么就放到低位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; // 低位数组
}
// 如果存放高位置的节点不是null 那么就放到高位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //加上原数组长度到达高位数组
}
}
1、当我们扩容之后元素需要重新定位元素中的位置通过(e.hash & oldCap) == 0) 算出元素是在高位数组还是低位数组中
例子1:假设当前数组长度为16要扩容到32,并且hashMap中存放了数据54
原来未扩容之前的数组的位置
110110 54
01111 16-1=15
0110 6
原数组的位置 6现在扩容之后按照原算法设置位置
110110 54
011111 32-1=31
10110 22
最后得到的结果22现在扩容之后的新算法数组的位置
10110
10000
10000
不等于0 代表的就是高16位的地方 原来数组的长度+原来数组的位置 =22最后的结果省去一次计算
2、重新排列时候的两个链表是怎么分化的?
// 低位头和尾巴
// 头记录了第一个元素的位置 也就是hash值
// 尾用于一个一个往后加元素 加到最后头就代表了整个元素
Node<K, V> loHead = null, loTail = null;
// 高位头和尾巴
Node<K, V> hiHead = null, hiTail = null;
.................
// 如果存放低位置的节点不是null 那么就放到低位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果存放高位置的节点不是null 那么就放到高位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
- 头和尾记录了相同的地址值
- 每次循环出一个新元素放到(高位或者低位)尾的下一个元素(用((e.hash & oldCap) == 0)计算低位还是高位)
- 最后低位链表放置到原位置不变(放置头中存放的地址值),高位链表的位置等于原位置+原数组的容量(放置头中存放的地址值)
get方法(没啥好说的直接看就能懂)
/**
* 获取元素值得方法
*/
public V get(Object key) {
Node<K, V> e;
/*调用的是下面的这个方法*/
// 原方法获取hash值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 获取key代表的链表中的节点
*/
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
// 如果桶的数量大于0并且待查找的key所在的桶的第一个元素不为空 这就证明可能在当前链表中
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 如果第一个元素的hash值相等 并且 第一个元素的key的地址值或者内容值相等那么久直接返回 // 检查第一个元素是不是要查的元素,如果是直接返回
if (first.hash == hash && ((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;
}
树化的方法treeifyBin(关于红黑树的部分比较复杂,还没咋看,简单看一下关键点)
/**
* 执行树化的逻辑
*/
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
// 如果桶数量小于64,直接扩容而不用树化
// 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
// 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素
// 这样即使扩容也无法减少链表的长度
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K, V> hd = null, tl = null;
do {
// 把所有节点换成树节点
TreeNode<K, V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 如果进入过上面的循环,则从头节点开始树化
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
- 总结:
- 如果桶数量小于64,直接扩容而不用树化
- 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
- 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素,这样即使扩容也无法减少链表的长度