HashMap源码分析 – 作者哇塞大嘴好帅
JDK1.7 HashMap源码分析
主方法
public static void main(String[] args) {
HashMap<String,Object> map = new HashMap<>();
}
首先点进去new HashMap<>();
/**
* 默认初始容量-必须为2的幂。
* 默认数组容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 在构造函数中未指定时使用的负载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 使用默认的初始容量构造一个空的HashMap
*(16)和默认负载系数(0.75)。
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
看到这个无参构造,调用了自身的有参构造
HashMap有参构造
/**
最大容量,如果隐式指定更高的值,
由两个带参数的构造函数组成。
必须是两个<= 1 << 30的幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 设置下一次的大小值 算法(容器 * 加载因子).
* @serial
*/
int threshold;
public HashMap(int initialCapacity, float loadFactor) {
//判断默认长度是否小于0 如果小于0就抛异常初始容器长度不合法
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);
/**
找到一个大于等于传递进来的容器大小的2次幂数
接受传递进来的容器大小,然后去设置容器大小,且容器必须是2的幂
capacity如果2次幂还小于传递进来的容器大小,那么就接着2次幂
1------ 0000 0001
2------ 0000 0010
4------ 0000 0100
16----- 0001 0000
.....
如果你传参为0那么就为1 因为capacity为1
**/
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//将加载因子复制给Hashmap的属性
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//完成数组的初始化
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//接下来去调用init() 在HashMap中他什么也不做
init();
}
这个方法就做了容器的初始化
put方法
public V put(K key, V value) {
//判断这个键是否为空 如果等于空就会进行初始化
//通过这行代码可以看到 hashmap的key值是可以等于null的
if (key == null)
//将数据存放在数组第0个位置
return putForNullKey(value);
//给key算出一个hash值
int hash = hash(key);
//拿出hash值 还有数组的长度 得到一个数组下标
int i = indexFor(hash, table.length);
/**
该循环做的事情就是,找到指定下标的链表,然后遍历链表,然后判断新插入的Hashmap的键值是否相同,如果相同就把原来的值返回,把新值覆盖。
Entry<K,V> e = table[i]; 获取到某个数组的下标的链表
e != null 如果不等于空就继续,如果等于空就代表这个链表探索玩了
e = e.next 链表的下一个
**/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断 hash值是否相等 判断key是否相当
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//先把原来的value拿出来
V oldValue = e.value;
//然后再把新的value覆盖进去
e.value = value;
//在HashMap不会有效果
e.recordAccess(this);
//当找到相同把原来的值返回
return oldValue;
}
}
//修改次数
modCount++;
// hash值 键名字 值 数组下标
addEntry(hash, key, value, i);
//如果没有找到相同的key值那么就返回null
return null;
}
addEntry方法
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//进行扩容
resize(2 * table.length);
//计算出hash值
hash = (null != key) ? hash(key) : 0;
//计算出下标
bucketIndex = indexFor(hash, table.length);
}
//添加
createEntry(hash, key, value, bucketIndex);
}
createEntry方法
/**
* 做了将一个数组存到hashmap里面去
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
//新创一个Entry对象 把他赋值到table[bucketIndex]的前面
Entry<K,V> e = table[bucketIndex]; //获取key值的下标
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
hash方法
final int hayihsh(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
//首先h会异或hashCode
h ^= k.hashCode();
/*
hash值算法
h = h 异或 (h二进制向右移动20位 异或 h二进制向又移动12位)
然后 h = h 异或 (h二进制又移动12位) 异或 h二进制又移动4位
*/
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor方法
static int indexFor(int h, int length) {
/*
* hash值 & 15
* 假设hash值二进制是 0101 0101
* leng-1 是 0000 1111
* 那么他们的结果就是 0000 0101
*/
return h & (length-1);
}
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++;
//这里他直接写死了 hash值为0 数组下标为0
addEntry(0, null, value, 0);
retu
remove方法
remove()方法
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value);}
removeEntryForKey方法
final Entry<K,V> removeEntryForKey(Object key) {
//获取hash值
int hash = (key == null) ? 0 : hash(key);
//获取指定下标
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
//遍历下标
while (e != null) {
Entry<K,V> next = e.next;
Object k;
//判断key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
//操作次数++
modCount++;
//容量减1
size--;
//把为1的元素删除掉
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
扩容
扩容的目的,是为了让链表变短,而不是单纯的移植下链表和扩容下数组
addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前存放的容量超过了预值
//预值算法 table.length*加载因子 并且当前 当前数组下标不等于null
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);
}
resize方法
void resize(int newCapacity) {
//记录下老数据
Entry[] oldTable = table;
//记录老数组的容量
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//新生成的一个数组 比如我们之前的数组容量是4,那么就变成了8
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//创建一个新的数组
transfer(newTable, rehash);
//把新的数组赋值给旧的数组,这样旧的数组旧变成新的数组了
table = newTable;
//计算出新的预值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer方法
void transfer(Entry[] newTable, boolean rehash) {
//拿到新的数组大小
int newCapacity = newTable.length;
//遍历旧的的数组,并且把旧的数组的值赋值到新的数组
for (Entry<K,V> e : table) {
/**
* 如果当前链表不等于空 那么就将旧链表的内容利用头插入一个一个的移植到新链表
**/
while(null != e) {
//next = 链表的下一个
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过hash值计算出新的数组的下标
/*
hash值 二进制 运算符
假设hash值为: 69 0100 0101 &
旧数组长度: 15 0000 0101
数组下标结果: 5 0000 0101
第一种可能
假设hash值为: 69 0100 0100 &
新的数组长度 : 31 0001 1111
数组下标结果: 5 0000 0101
第二种可能
假设hash值为: 85 0101 0101 &
新的数组长度: 31 0000 1111
数组下标结果: 21 0001 0101
通过以上两个例子,可以得出,将旧数组的链表的移植到扩容后的新数组下标中可能存在2个位置,一个位 置是旧数组所在的下标,或者 旧数组的所存在的下标 + 旧数组的长度。
*/
int i = indexFor(e.hash, newCapacity);
/**
* 采用了头插法
**/
//将旧的数组元素指定到新的数组指定下标
e.next = newTable[i];
//将e赋值给新数组计算出来的指定下标
newTable[i] = e;
//然后在将e指向旧链表的下一个
e = next;
}
}
}
1.7 HashMap 多线程安全隐患
jdk7里面多线程扩容的时候可能会导致循环列表的出现
出现此问题的原因,扩容的时候使用的头插法导致的
解决方案:控制预值。 预值算法 = 数组容量*加载因子
假设2个对象同时调用put方法,当执行到
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);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
void resize(int newCapacity) {
//记录下老数据
Entry[] oldTable = table;
//记录老数组的容量
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//新生成的一个数组 比如我们之前的数组容量是4,那么就变成了8
Entry[] newTable = new Entry[newCapacity];
// useAltHashing 由在创建HashMap静态类赋值。
boolean oldAltHashing = useAltHashing;
/**
* isBooted() 默认值为false
* newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 为 true
**/
useAltHashing |=
sun.misc.VM.isBooted()&&(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
/**
如果 表达式一真一假 rehash为真
认为当我的数组的容量超过某一个值的时候就去生成一个hash总值
**/
boolean rehash = oldAltHashing ^ useAltHashing;
//传递一个新的数组
transfer(newTable, rehash);
//把新的数组赋值给旧的数组,这样旧的数组旧变成新的数组了
table = newTable;
//计算出新的预值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
HaspMap静态方法
static {
//判断虚拟机有没有jdk.map.althashing.threshold环境变量的值
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
/**
如果 altThreshold 不等于空
就取虚拟机设置的值
如果虚拟机里没值就取默认的值
**/
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// 如果等于-1 就拿Integer.MAX_VALUE
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
HashMap.class.getDeclaredField("hashSeed"));
} catch (NoSuchFieldException | SecurityException e) {
throw new Error("Failed to record hashSeed offset", e);
}
}
}
程序运行前
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SNH1ZO6-1629305825824)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530203637685.png)]
第一个线程执行后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BuwBo5cn-1629305825827)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530204123865.png)]
这时候我们会发现第一个存在了这三个元素,第二个线程确没有存在,
第二个线程执行
第一次循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weShw561-1629305825829)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211012812.png)]
第二次循环
Entry<K,V> next = e.next;操作后的图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7HzuTUI-1629305825831)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211107117.png)]
第二次循环执行后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeD3Ghve-1629305825832)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530210332361.png)]
第三次循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLuLhLZx-1629305825833)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211954817.png)]
当执行到 e = next; 这时候e == null 就会结束循环。
当执行到table = newTable;
第一个线程没有问题。
当第二个线程赋值就会出现问题[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sHZOlve-1629305825834)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211903929.png)]因为他们已经成了一个循环链表
HashMap常犯错误
ON.1
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("2","2");
map.put("1","1");
Iterator iterator = map.keySet().iterator();
while (iterator.hasNext())
{
String key = (String) iterator.next();
if (key.equals(key))
{
map.remove(key);
}
}
}
}
报错信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yLEvGBF-1629305825834)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530234510883.png)]
为什么会有异常?
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
如果我们**key.equals(“1”)**那么就不会有异常,因为哪个时候 modCount = 2. expectedModCount = 2;
modCount 默认值为0 两次put方法 一个remove方法这时候他的值为3 expectedModCount = 2,
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
KeyIterator 继承了 HashIterator就会执行他的构造类
HashIterator() {
//赋值给他操作次数
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
因为以上的操作HashMap会认为不是安全的做法他会利用它的快速报错的机制
正确的做法
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("2","1");
map.put("1","1");
map.put("3","1");
Iterator iterator = map.keySet().iterator();
while (iterator.hasNext())
{
String key = (String) iterator.next();
if (key.equals("3"))
{
iterator.remove();
}
}
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
这个方法的区别就是他调用玩删除会重新赋值下