HashMap是最常用的集合之一。
他的存储结构其实就是一个Entry类型的bucket数组。
而Entry其实就是一个链表,包含四个属性,hash,next,key,value
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
创建
先从创建开始看
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();
}
可以看出,创建的时候有两个变量影响着hashmap,一个是initialCapacity(初始容量),一个是loadFactor(加载因子),一般来说,决定hashmap是否扩容是通过容量*加载因子之后的值来决定的。
而对于扩容可以直接看代码
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);
}
每一次put的时候,如果hash和bucket没有重复,那么当新添加一个Entry元素的时候,一定会查看是否需要扩容,扩容是2的次幂。
而put的代码如下,如果不是重复key或者hash碰撞会调用addEntry方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//i是作为bucket数组下标存在,indexFor()是将hash和 table.length进行与运算,防止hash过长,table则是存放Entry链表的bucket数组
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;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
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);
}
addEntry里面会调用resize方法,其中的核心就是transfer
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//以下就是将当前元素放入重新计算的bucket位置的头部,并且将当前变量换成当前元素变更前的next
//取出当前Entry元素的下一个节点
Entry<K,V> next = e.next;
//一旦扩容,重新计算hash和bucket位置
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//将当前Entry元素的next指向对应bucket的头节点
e.next = newTable[i];
//当前Entry元素放入对应bucket头部
newTable[i] = e;
//下一次对当前元素修改前的next节点进行transfer操作
e = next;
}
}
}
但是随之而来的可能会有并发问题,主要是两个方面,一是两个线程同时put同一个bucket里的entry,一个会覆盖另一个
二是当同时扩容调用transfer的时候,会导致entry.next死循环
详细可以参照https://mp.weixin.qq.com/s?__biz=MzIxMjE5MTE1Nw==&mid=2653192000&idx=1&sn=118cee6d1c67e7b8e4f762af3e61643e&chksm=8c990d9abbee848c739aeaf25893ae4382eca90642f65fc9b8eb76d58d6e7adebe65da03f80d&scene=21#wechat_redirect
而hashTable则是在对应的操作方法前增加了synchronized关键字,但是因为直接锁住了整个hashMap对象,所以效率低下,因此有了ConcurrentHashMap,ConcurrentHashMap将在下一篇写
本文深入解析了HashMap的工作原理,包括其内部存储结构、初始化过程、扩容机制及潜在的并发问题。
1177

被折叠的 条评论
为什么被折叠?



