Java7–HashMap
文章目录
常量
// 默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**表未膨胀时要共享的空表实例。
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
/**表,根据需要调整大小。长度必须始终是 2 的幂。
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**此映射中包含的键值映射的数量。
*/
transient int size;
/**
* 要调整大小的下一个大小值(容量负载因子)。 阈值, 是通过加载因子算出来的
*/
// If table == EMPTY_TABLE 那么这是初始容量
// 膨胀时将创建table。
int threshold;
/**加载因子
*/
final float loadFactor;
构造方法
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
* 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
调用无参构造会去调用有参构造, 传入: 默认容量16, 默认加载因子 0.75
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; //默认0.75
threshold = initialCapacity;//默认16
init();
}
/** 子类linkedlist中有用
子类的初始化挂钩。在初始化 HashMap 之后但在插入任何条目之前,在所有构造函数和伪构造函数(clone、readObject)中调用此方法。 (在没有这个方法的情况下,readObject 需要明确的子类知识。)
*/
void init() {
}
- 进行一系列判断赋值
put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //判断当前的table是否为空
inflateTable(threshold); //1,为空就初始化, -----------------------------------------详细方法在下面1-------------------
}
if (key == null) // HashMap的key可以为null
return putForNullKey(value);//2,相当于key为null 放到数组的下标0的位置-----------------详细方法在下面2-------------------
int hash = hash(key); //4,计算hash值----------------------------------------------------详细方法在下面4-------------------
int i = indexFor(hash, table.length); //3,拿hash值和数组长度获得一个数组下标--------------详细方法在下面3-------------------
//Entry<K,V> e = table[i];-------------循环从table[i]开始,链表头节点
// e = e.next------------取下一个节点
//e != null; ------------ 说明取到最后一个节点, 遍历所有元素
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断 hash值是否相等 && key是否相等
//hash值相等, key不一定相等, hash值不相等, key一定不相等
//key不相等, hash值一定不相等 , 所以先比较hash ,为false可以直接跳过,节省
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//如果有key相同的就覆盖
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//HashMap没有使用这个方法,是为了子类使用
//赋值完成后返回旧的value
return oldValue; //找到之后就return, 好处:不用每次都遍历到尾部
}
}
modCount++;//9代表修改次数,跟异常有关-----------------------------------------------------详细解释在下面9--------------------
addEntry(hash, key, value, i);//5,添加元素------------------------------------------------详细方法在下面5-------------------
return null; //put方法如果不重复返回null
}
1,初始化方法
//1, 初始化方法
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 找出>=2的幂次方的数, 2, 4, 8, 16 ...
// 比如传进来10 , 返回16, 16返回16
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
//计算
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
}
如何获得一个大于等于2的tosize的 2的幂次方数
//找规律
1 -------- 0000 0001
2 -------- 0000 0010
4 -------- 0000 0100
8 -------- 0000 1000
16 ------- 0001 0000
//二进制中, 只有一个bit点为1 的数
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;
}
public static int highestOneBit(int i) { //传进来一个17
// HD, Figure 3-1 |运算 有1填1
i |= (i >> 1); /* 17 0001 0001
>> 1 0000 1000
| 0001 1001
*/
i |= (i >> 2); // >> 2 0000 0110
// | 0001 1111
i |= (i >> 4); // >> 4 0000 0001
// | 0001 1111
i |= (i >> 8); // >> 8 0000 0000
// | 0001 1111 不会变了
i |= (i >> 16);
return i - (i >>> 1); //>>1 0000 1111
//i- 0001 0000 ----16
}
System.out.println(Integer.highestOneBit(6)); //4
System.out.println(Integer.highestOneBit(7)); //4
System.out.println(Integer.highestOneBit(8)); //8
System.out.println(Integer.highestOneBit(10)); //8
System.out.println(Integer.highestOneBit(16)); //16
传入的数字 001* ****
>>1 0001 ****
| 0011 ****
>>2 0000 11**
| 0011 11**
>>4 0000 0011
| 0011 1111 最后都会把高位1后面的*全部变成1
0010 0000 最后执行的i - (i >>> 1);
相当于是获取传入的数子高位后面全是0的数
001* ****
0010 0000 所以就获得了2 的幂次方的数
为什么要右移 8 16 这么多?
因为int 4字节 32个bit位
再回到roundUpToPowerOf2方法
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;
}
//17--highestOneBit--16
//我们想要的结果是 17----32
//加了一个表达式(number - 1) << 1)
//比如17<<1 = 17*2 = 34--highestOneBit-- 32
// 15<<1 = 30--highestOneBit--16
//这样就能达到目的了
再回到初始化方法来
//1, 初始化方法
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 找出>=2的幂次方的数, 2, 4, 8, 16 ...
// 比如传进来10 , 返回16, 16返回16
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
//计算
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//现在我们已经获取到了一个>= tosize 的 2的幂次方数
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; //已16作为数组的大小创建
initHashSeedAsNeeded(capacity);
}
}
2, key==null调用putForNullKey
private V putForNullKey(V value) {
//key = null ,相同的话覆盖
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);// 相当于key为null 放到数组的下标0的位置
return null;
}
3,计算下标
//a , indexfor
static int indexFor(int h, int length) {
return h & (length-1);
}
h: 0101 0101
15: 0000 1111
& 0000 0101 取值范围是 0000-1111 0-15
为什么数组的最大值要是2的幂次方数
好处,算下标的时候 取& 比取% 位运算效率高一点
4,通过key计算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();
//此函数确保在每个位位置仅相差恒定倍数的 hashCode 具有有限数量的冲突(在默认负载因子下约为 8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
h: 0101 0101
15: 0000 1111
& 0000 0101 取值范围是 0000-1111 0-15
//由上面看出, 高位的不影响结果, 所以 需要进行右移, 异或, 让高位参与进算法中来
//为了让链表不那么长
//为了让hashcode更散列一点
5,添加元素addEntry(hash, key, value, i);
//添加元素
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断数组大小是否大于阈值 , 扩容 threshold = (table.length)16 * 0.75
//null != table[bucketIndex 表示 数组当前索引的值不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //6扩容方法-翻倍----------------------------方法在下面6-----------------------
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//创建entry放入数组
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 头插法 e这里是next
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++; //添加之后大小++
}
6,扩容resize
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));//7转移元素-----------------------详细方法在下面7--------------------------
table = newTable;//再把原来的数组重新赋值成扩容后的数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//再重新计算阈值
}
可以看到上面第3步的计算下标的公式
//a , indexfor
static int indexFor(int h, int length) {
return h & (length-1);
}
h: 0101 0101
15: 0000 1111
& 0000 0101 取值范围是 0000-1111 0-15
通过下面位运算可以看出,扩容后存放新数组的下标会出现两种情况(不rehash情况)
- 和老数组的下标一致, index
- 老数组下标+原数组长度length(默认16)= old-index + old.length
7,扩容后转移元素(transfer)
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) { //1.8没有这个判断了---------------------详细方法在下面8-----------------------------
e.hash = null == e.key ? 0 : hash(e.key);
}
//转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//把新元素插到链表头部
newTable[i] = e; //再把这个元素作为新头部, 实现下移
e = next;//上面完成了1个元素转移, 指向下一个继续循环转移
}
}
}
1.7多线程扩容可能出现循环链表, 最终导致put/set 进入死循环(CPU100%)
两个线程同时进入转移方法时transfer
都会进入循环遍历老数组的元素
发现第一个元素已经把老数组的元素转移到了新数组里, 而且链表顺序反了过来
第二个线程的变量, 就会指向第一个线程转移后的地址, 接着指向后面的代码
if (rehash) { //1.8没有这个判断了
e.hash = null == e.key ? 0 : hash(e.key);
}
//转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//把新元素插到链表头部
newTable[i] = e; //再把这个元素作为新头部, 实现下移
e = next;//上面完成了1个元素转移, 指向下一个继续循环转移
第二次循环后
e2 , 和 next2 重新指向
走完之后
出现了循环链表
第二个线程再把数组赋值到这个新数组table = newTable;
下次再指向put/get时, 进入循环链表的话就会死循环
原因:
- 第二个线程的e2 和next2 位置翻了过来, 主要是因为使用的头插法导致了链表顺序反了过来
怎么避免扩容出现的问题
- 1防止他进行扩容, 控制他的阈值, threshold,确定数据大小, 设置好阈值
- 2多线程操作时做一些操作, 如加锁
8, rehash(转移元素transfer里调用)
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) { //1.8没有这个判断了
e.hash = null == e.key ? 0 : hash(e.key);
}
//转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//把新元素插到链表头部
newTable[i] = e; //再把这个元素作为新头部, 实现下移
e = next;//上面完成了1个元素转移, 指向下一个继续循环转移
}
}
}
//是由调用transfer 传进来的
transfer(newTable, initHashSeedAsNeeded(newCapacity));
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;
}
//是有jdk.map.althashing.threshold这个属性决定的
private static class Holder {
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
- 如果数组容量 capacity >= jdk.map.althashing.threshold , hash种子就会被赋值
- jdk.map.althashing.threshold作用, 就是可以觉得hash算法不够, 可以加一个系统变量来让hash算法更加复杂
- 添加vm参数
-Djdk.map.althashing.threshold=3
生成hash种子
9,modCount++; 代表修改次数
删除key =2 时会出现异常
反编译后, 发现是使用迭代器
父类迭代器,
第二次循环modcount 已经++ = 3, 而expectedModCount还是2
再次判断不相等之后就会抛出异常
怎么解决这个问题呢
使用迭代器的remove方法
迭代器的remove方法会重新给modecount进行赋值, 就不会出现异常了
为什么要使用modecount来抛出异常呢
- 快速失败, 发现错误让程序快速停止
- 比如两个线程同时操作时, 一个线程正在读取, 一个线程在修改, 就会出现问题 ,所以就会抛出异常
为什么用头插法
- 都需要遍历链表,所以头插尾插都差不多
数组和链表插入效率
- 实际情况数组和链表的插入效率不一定谁快谁慢
- 还是要根据实际情况
- 数组插入时会把元素往后移动, 而链表需要定位到指定位置 再执行插入, 数组也需要定位, 但是由于自身特性有索引, 比较快
get方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通过hash算法计算出hash值
int hash = (key == null) ? 0 : hash(key);
//再通过hash值计算出数组下标, 再进行链表遍历比较key的值
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;// 找到就返回
}
return null;
}
HashMap的get方法是怎么获取到数据的
get(key);
- 1, 通过key.hash获取到hashcode , 在进行一系列操作右移异或 &等操作获得数组下标
- 2, 比对数组下标的key是否相等, 不相等在比较是否有下一个元素(next) 找到为止