HashMap是我们平时开发经常用的集合,,虽然我们没有那么多的数据需要处理,需要花大时间考虑性能优化,但理解了它的存取数据的方式,对开发也会有所帮助
HashMap是数组加单向链表组成的双列集合, 它允许key为null,value为null。
成员,常量和方法
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16初始容量
static final int TREEIFY_THRESHOLD = 8;//jdk8新增,链表长度>8后,转为红黑树存储
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
int threshold;// // 扩容的临界值,阈值。size>=threshold就会扩容
final float loadFactor;//加载因子
transient int size;// // 已存元素的个数,map.size()返回的值
transient int modCount;//结构修改 次数记录
transient Node<K,V>[] table; // hash数组,哈希表,长度设定为2的幂
transient Set<Map.Entry<K,V>> entrySet;//
static class Node<K,V> implements Map.Entry<K,V> {//元素
final int hash;// 通过hash(Object key)计算出来key的哈希值
final K key;
V value;
Node<K,V> next;}//链表下一个元素
public final int hashCode() {/元素的hasCode /key的hashCode 和 value的hashCode 亦或得到
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
static final int hash(Object key) {//计算key的哈希值的方法:key的hash会经扰动函数,避免hash取余后在数组index相同的几率,让高位也参与hash生成
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}构造方法
构造方法共有4个,其中带2参的此构造方法包含了主要功能,在此只分析这一个(
构造一个带指定初始容量和加载因子的空 HashMap)
//参数一,初始容量。参数二,加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//检查参数合法性
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//容量最大1<<30,10亿左右
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//iaNaN()检查float合法性
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;//设置加载因子
this.threshold = tableSizeFor(initialCapacity);//计算并设置阈值
}put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);//调用下一个方法
}
//onlyIfAbsent: 如果该位置存在value,put方法是否覆盖它
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; //哈希表
Node<K,V> p;
int n, i;//n为哈希表长度。i为传入的key再表中位置
if ((tab = table) == null || (n = tab.length) == 0)//如果是初次
n = (tab = resize()).length;//走初始化方法,并将新建表的长度赋值给n
if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash是计算哈希值在表中位置,与hash%n等价.此处将表此位置的元素赋给p,在p==null的if条件小
tab[i] = newNode(hash, key, value, null);//直接将表的该位置存储该元素
else {//如果该位置元素不为null,下面会判断是该替换还是在链尾追加
Node<K,V> e; K k;//e用来存旧位置的元素,k用来存就位置的key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//key相同。k存上了旧位置的key
e = p;//e存上了旧位置的元素
else if (p instanceof TreeNode)//key不同,且是红黑树节点,走红黑树逻辑的put方法,暂不深究
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//key不同,且还是普通链表
for (int binCount = 0; ; ++binCount) {//for循环无终止条件
if ((e = p.next) == null) {//e存上了p的next()且其为null的条件下(是尾了)
p.next = newNode(hash, key, value, null);//链尾加上新值
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果长度超过8,链转为红黑树,不深究
treeifyBin(tab, hash);
break;
}
//p不是链尾,且,e的key跟put的key相同,会走到下边的e.value = value;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//e的key跟put的key不同,p置为新的e,继续下一轮循环
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;//e的value设为传进来的value
afterNodeAccess(e);//此方法空实现,埋给linkedhashmap用的
return oldValue;//return旧的value,且此情况下modcount没发生改变
}
}
//走到以下的条件为,是追加新的节点,而非替换value
++modCount;//此时预示着结构改变了(非替换旧value的情况下)
if (++size > threshold)//如果增加一个,后的size>阈值
resize();//表重构
afterNodeInsertion(evict);//空实现
return null;//
}重构大小
//返回新重构后的数组哈希表
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//
int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧的数组长度
int oldThr = threshold;//旧的阈值
int newCap, newThr = 0;//新的数组长度,阈值
if (oldCap > 0) {//旧的数组长度>0
if (oldCap >= MAXIMUM_CAPACITY) {///如果上限了
threshold = Integer.MAX_VALUE;//阈值调为最大值
return oldTab;//哈希表不再扩张了
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&//新数组长度扩大一倍,
oldCap >= DEFAULT_INITIAL_CAPACITY)//并且旧的数组长度>16条件下
newThr = oldThr << 1; // double threshold新的阈值也扩大一倍
}
else if (oldThr > 0) //旧的数组长度=0 且旧的阈值>0
newCap = oldThr;//新数组长度=旧的阈值长度
else {// zero initial threshold signifies using defaults,第一次旧的数组长度为0条件下
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值设为0.75x16=12
}
if (newThr == 0) {//新的阈值仍=0
float ft = (float)newCap * loadFactor;//新的表长度x加载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?//在最大值范围内
(int)ft : Integer.MAX_VALUE);//赋值给新的阈值
}
threshold = newThr;//新的阈值赋值到成员阈值中
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//利用新计算的哈希表长度new个新表
table = newTab;//新哈希表赋给成员哈希表
if (oldTab != null) {//非空条件下,开始高低位移动了
for (int j = 0; j < oldCap; ++j) {//for循环旧哈希表
Node<K,V> e;
if ((e = oldTab[j]) != null) {//e暂存数组的第一个元素
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 { ///如果下一个元素非空,要高低位移动了
Node<K,V> loHead = null, //低的头loTail = null;//低位的尾
Node<K,V> hiHead = null, //高的头hiTail = null;//高的尾
Node<K,V> next;
do {
next = e.next;//暂存下一个元素到next
if ((e.hash & oldCap) == 0) {// (等于0代表小于oldCap,应该存放在低位,否则存放在高位
) if (loTail == null)
loHead = e;//赋值e到低头
else
loTail.next = e;//低尾的下一个存上e
loTail = e;//赋值e给新低尾
}
else {//如果不是在哈希表0位
if (hiTail == null)//高尾为空时
hiHead = e;//e赋值给高头
else//高尾非空时
hiTail.next = e;//高尾的下一个存上e
hiTail = e;//赋值e给新的高尾
}
} while ((e = next) != null);//e下一个不为空时,继续循环
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;//跟扩容前相同的位置存上低头
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;//扩容后的位置: 存上高位的头
high位= low位+原表长度
}
}
}
}
}
return newTab;//返回新表
}总结
数组的扩容永远为2的倍数, << 1; // double方便取余位运算%
jdk8,>8后转为红黑树
遍历顺序:数组+链表
根据索引查找元素(getKey)的时候,两步:1,通过hasHash(Key)快速的找到数组哈希表索引,2慢速的遍历链表
扩容时建一张新表,并将旧表数据全部移到新表.:low位索引不变,top位索引等于原索引+原数组长度
zyt