initialCapacity
容量,默认是16,用来决定Entry数组的长度,Entry数组的长度是一个大于或等于initialCapacity的2的n次方数
loadFactor
加载因子,默认0.75,和Entry数组的长度一起来决定阈值threshold
threshold
阈值,threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1),阈值是用来决定是否扩容的条件
hashSeed
hashSeed来计算并判断的,hashSeed默认为零,判断是否需要给haseSeed赋值是通过判断容量是否大于某个配置的环境变量值(如果没有配置这个值,默认为Integer.MAX_VALUE), 所以默认情况下hashSeed都为0,rehash都为false。hashSeed的作用是通过影响hash算法来让元素分布的更散列
初始化
初始化操作主要是用来对loadFactor和threshold进行赋值操作
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;
threshold = initialCapacity;
init();
}
put方法
put方法主要做的事:
- 如果table数组是空,就计算数组容量,threshold和hashSeed,并创建一个新的Entry数组
- 如果key是null,则去table[0]去遍历链表,如果有key为null的就替换,否则创建一个新节点用头插法插入到链表中
- 通过hash值计算key在table中的下标,遍历链表如果找到了key相等的就返回
- 如果链表中没有相等的则区创建一个并用头插法插入到链表中,创建之前先去判断是否需要扩容,如果需要扩容则先扩容并把老数组中的元素转移到新数组中
public V put(K key, V value) {
// 如果Entry数组是空数组就创建
if (table == EMPTY_TABLE) {
// 计算数组容量,创建Entry数组,计算threshold和hashSeed
inflateTable(threshold);
}
if (key == null)
// 如果key是null,则去table[0]去遍历链表,如果有key为null的就替换
// 否则创建一个新节点用头插法插入到链表中
return putForNullKey(value);
// 先用hashSeed和key.hashCode进行抑或得到h
// 然后再对h进行多次右移和抑或操作
// 右移操作是为了让高位也参与运算
// 多次右移和抑或操作是为了让hash值更加的分散
int hash = hash(key);
// 通过hash值计算key在table中的下标
// 计算方式为 hash & (table.length - 1)
int i = indexFor(hash, table.length);
// 遍历链表如果找到了key相等的就返回
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数加1
modCount++;
// 没有相等的则创建一个并头插法插入到链表中
addEntry(hash, key, value, i);
return null;
}
inflateTable
private void inflateTable(int toSize) {
// 找到一个大于或等于toSize的2的n次方数
int capacity = roundUpToPowerOf2(toSize);
// 计算threshold
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
// 初始化hashSeed
initHashSeedAsNeeded(capacity);
}
roundUpToPowerOf2
private static int roundUpToPowerOf2(int number) {
// 如果number大于或等于MAXIMUM_CAPACITY则number = MAXIMUM_CAPACITY
// 如果number小于1则number = 1
// 如果number大于1,则先将(number - 1)左移一位即扩大两倍
// 然后再调Integer.highestOneBit方法找到比它小的最大的2的n次方数(这个操作有点sao)
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
initHashSeedAsNeeded
final boolean initHashSeedAsNeeded(int capacity) {
// 默认为false
boolean currentAltHashing = hashSeed != 0;
// jvm是否启动且容量是否大于Holder.ALTERNATIVE_HASHING_THRESHOLD
// Holder.ALTERNATIVE_HASHING_THRESHOLD于"jdk.map.althashing.threshold"这个配置有关
// 如果配置了就取这个值,如果没配置就取Integer.MAX_VALUE
// 此处默认为false
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
// 此处默认为false,所以hashSeed默认为0
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果size大于等于阈值且当前链表的头节点不是空
// 那也就是说阈值并不是唯一确定是否扩容的条件
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容并将元素转移到新数组中
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 创建元素并用头插法插到链表中
createEntry(hash, key, value, bucketIndex);
}
扩容
扩容的是为了让链表的长度变短,提升查询效率,transfer方法会出现循环链表问题,导致put的时候出现死循环,因为头插法在扩容时会出现链表逆序的问题,在多线程扩容的情况下会造成循环链表问题,
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果老数组的容量达到了最大容量就不再扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个两倍容量的新数组
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer方法
- transfer方法主要是用来将老数组的元素转移到新数组,其中有rehash标识依赖于initHashSeedAsNeeded方法的返回值,上面已经分析过该方法的返回值默认为false,所有默认情况下是不需要rehash的,而且在jdk1.8中直接把rehash的判断去掉了
- 将老数组的元素转移到新数组后元素顺序变成了逆序
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 用原来的hash值计算出来的结果等同于 newIdx = oldeIdx + oldTable.length
int i = indexFor(e.hash, newCapacity);
// 插入到新数组的顺序变成了逆序
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容引发的循环链表问题
出现循环链表的情况:
modCount
修改次数,modCount的作用是fast-fail容错机制
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
// 返回的是KeyIterator对象
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
// hashMap.keySet()返回的是KeySet对象,KeySet对象内部的iterator会返回一个KeyIterator对象
// KeyIterator继承的是HashIterator,当调用KeyIterator的next方法时会调用HashIterator的nextEntry方法
// 此时会判断modCount和expectedModCount是否相等,不相等就会报错
final Entry<K,V> nextEntry() {
// 此时会判断这两个值是否相等
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
for(String key : hashMap.keySet()) {
// 调用的hashMap.remove会修改modCount的值,但是不会修改expectedModCount的值
hashMap.remove(key); // 此时会抛异常
// 解决办法是使用iterator的remove方法,此时会调用HashIterator的remove方法
// remove方法内部会调用hashMap的remove方法并让expectedModCount = modCount
}