HashMap
前沿:首先HashMap具体实现是由链表+数组的实现方式,并且采用了动态扩容技术
主要对象
table
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
注意是transient,非序列化
1.数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义
2.不同的虚拟机对于相同 hashCode 产生的 Code 值可能是不一样的
Entry
Entry<K,V>是链表和数组的主要组成:
其重要的成员变量是Key,Value,Entry<K,V>next,hash
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
```
构造如下:
其他组成:
transient int size;//实际个数
int capacity;//实际大小
int initialCapacity,//初始的数组大小
int threshold;//capacity*loadFactory。HashMap put新元素后,如果size>threhold,需要扩容
/**负载因子,代表了table的填充度有多少,默认是0.75
加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;
/*HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException/
transient int modCount;
HashMap方法:
1:构造方法
//主要是对传入的initialCapacity和loadFactor进行参数检验,没有为数组table分配内存空间而是在执行put操作的时候才真正构建table数组
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();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
}
2:put
主要分为四部分:
1:如果table为空,需要创建table
2:计算机hash值和数组的index
3:如果在数组中存在,则替换并返回旧hash
4:如果在数组中不存在,则直接插入数组
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(16)==后面会改变==
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
//如果该对应数据不存在,直接加入
addEntry(hash, key, value, i);//新增一个entry
return null;
}
其中使用的方法:
inflateTable(),hash(),indexFor(),recordAccess(),addEntry()
hash():转为hash值
addEntry():加入hash数组,并且根据情况进行扩容
如何初始扩容:
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
/**此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 */
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
如何根据hash确定索引:
static int indexFor(int h, int length) {
return h & (length-1);
}
关键点:为什么实际大小总是2的幂?
我的理解:
原因一:
因为index=hash%length-1
比如对于:
length=16 则length-1 =15 即 00001111
length=32 则length-1=31 即 00011111
可以看到,对于一个相同的hash,分别&两个不同的length-1,得到的结果result1,result2 差别就只在右数第5位,相当于扩容后只要修改一位就可以改变索引
原因二:
因为length=2^n,则length-1的位都是保持00001111111的形状,而hash&length-1中,高位不会产生影响,而低位任意一个变化变化都会产生影响,减少冲突的概率。
在h<length的情况下:
对于长度:23 length-1则比特为 10110,则对于结果result=10110
既可以是h=11110 10110 11111 10111 都对应相同的结果冲突增加
而对于长度32 length-1 则比特位11111 ,则对于结果10110
只有h=10110才可以与其对应
其实HashMap还要其他方法,但是感觉关键部分就是上面写的,如果还想加深了解,可以看一下