一、构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当初始容量太大,大于了允许的最大值时,使用最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子必须是大于0,而且必须是数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//初始化时将一个Map集合放入新创建的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);putAllForCreate(m);
}
二、put方法
public V put(K key, V value) {
//当第一次放入元素时,开始做初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//这里是得到通过key计算出来的hash值,这个hash值通过
//位移运算和hashseed进行位运算得到
int hash = hash(key);
int i = indexFor(hash, table.length);
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;
}
}//和快速失败有关
modCount++;
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
1、roundUpToPowerOf2
获得大于给定容量最接近的2的次幂。比如,tosize为7 经过roundUpToPowerOf2后得到8。
原理:先将(number - 1) << 1,通过左移1位,让number变大后,再调用highestOneBit找到最终的值。
Integer.highestOneBit(获取小于给定i的最大2的次幂)
public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1); }
案例,比如现在i为20
(1)右移1位
i: 0001 0100
i>>1:0000 1010
|: 0001 1110
(2)右移2位
0001 1110
i>>2:0000 0111
|: 0001 1111
(3)右移4位
0001 1111
i>>4:0000 0001
|: 0001 1111
(4)右移8位
0001 1111
i>>8:0000 0000
|: 0001 1111
(5)右移16位
0001 1111
i>>16:0000 0000
|: 0001 1111
执行i - (i >>> 1),结果为16
i: 0001 1111
i>>>1:0000 1111
-: 0001 0000
2、hash
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
将key的hash值计算出来然后做位运算,看代码知道位移了20、12这些,为什么要移动这么多,其实核心思想就是避免出现太多了hash冲突,你想哈,如果不位移这么多,那么计算出来的hash值大多数都一样,因为高位都是0,所以这样就会导致一个问题就是hash冲突太多,链表太长,所以位移位数多了以后,尽量将避免hash冲突。
3、indexFor
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
不妨带进来一个数算一遍,假设 h 的二进制表示是1101 0110,数组的长度是8,二进制就是0000 1000,这时先进行length - 1的操作,得到0000 0111,这时再与 hash 进行&操作时,可以得到0000 0110,即十进制的6,而 HashMap 的容量,即数组的长度永远都是2的次方,也就是说,table.length的二进制表示永远都是一个1,其余都是0的状态,例如2的4次方16是0001 0000,5次方32是0010 0000,那也就是说明,table.length - 1得到的值永远都是前一半都是0,后一半都是1,这种结构再与 hash 进行&操作时,得到的结果就和hash % table.length一样了。为了避免hash冲突,因此要在计算hash值时,尽量散列开。
此处也解释了为什么在初始化hashmap容量时必须要获取到大于指定size的最小的2次幂。
4、putForNullKey
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
上面这个方法当key==null的时候调用,HashMap允许传入一个null作为key,至于为什么这样做,可能为了支持更好,而且如果是null的key,那么默认放在第一位,也就是数组为0的位置,那么这里会出现一个疑问就是,当你放置null key的时候,第0个位置已经被占用了,那么怎么办,这个时候就会存在第0个位置的链表上。
代码可以看出for循环中是从数组的第一个位置开始循环的,也就是说key = null的数据是放在数组为0的位置上或者数组为0的链表上;上面的这个方法是要返回一个值得,如果说我们添加key = null的数据的时候,这个null = key已经有了,那么会替换这个新的值,然后返回之前的值,所以HashMap的put方法是有返回值的,如果返回值不为空,则原来的HashMap中已经存在了这个key,并且已经覆盖了新的值,并且将旧值返回,如果返回null,则Hashmap中没有这个值存在。
5、addEntry
同一个Entry上的链表上采用头插法新增新的Node
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容的大小为原来数组长度的2倍,比如当前长度16,扩容后就是32
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建一个Entry存放我们添加的元素
createEntry(hash, key, value, bucketIndex);
}
三、扩容
//这个newCapacity默认是扩容前的2倍,
void resize(int newCapacity) {
//首先声明一个Entry数组用来存放原来的数组信息
Entry[] oldTable = table;
//得到原来的数组长度,然后判断扩容的大小是否已经达到了最大的长度,
//如果大于了数组的最大长度,那么就设置阈值为最大数组的长度,则下次无法再扩容了
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//声明新的数组信息
Entry[] newTable = new Entry[newCapacity];
//数据的元素转移,就是讲oldTable转移到newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
//扩容的核心方法,就是讲原来的数组复制到新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//这里使用了2层循环来进行数组的复制,为什么要使用2层循环呢?
//因为hashmap是一般的数组结构,在数组元素上的单向链表结构,所以如果发生了数组
//的扩容,需要两层循环来操作
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//hashSedd的判断
if (rehash) {
//使hash更散列一些
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//通过hash计算出来的下标,
//在新的数组中赋值给原来数组的next
//其实就是新的数组下标引用了原来的下标数据的引用地址
e.next = newTable[i];
//将本次元素与原来解除关系过后,将引用变成原有的地址
newTable[i] = e;
e = next;
}
}
}
1、initHashSeedAsNeeded
该方法用来判断需不需要生成hashseed
当 capacity的值大于Holder.ALTERNATIVE_HASHING_THRESHOLD时,就生成一个hashseed,让hash算法的散列性,hashseed 的作用,让所得到的hash值更加散列,jdk8已经废弃。
Holder内部类代码如下
2、transfer
扩容迁移图示如下
多线程扩容的问题
假设线程2扩容运行到Entry<K,V> next = e.next
这行代码时阻塞,线程一完成扩容
线程二继续运行第一次循环
四、modCount(Fast-Fail机制)
表示每次对map的entry进行修改操作时都会对modCount做自增。
用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
五、有用的文章
(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析_王伟的博客-CSDN博客_hashmap1.7和1.8的区别