关于阅读HashMap源码的基本思路
Hashmap既然作为数据结构的存储容器,想必在其内部有一定实现来用于存储数据,因此我们首先要看的就是Hashmap中数据是以何种的形式存储,打开Hashmap源码发现有一个table变量,其数据类型为Node,hashmap是以key,value的形式存储数据,我们看下node方法发现,里面确实有存储key和value,而next是维护节点上下之间的关系,类似于指针的用法。
Hashmap中常用的方法分析
如果要说我们使用hashmap最常用的方法就是put方法。这是我们经常使用的往容器中添加数据的基本方法。下面我会贴上hashmap 1.7版本以及1.8版本 然后一一对其做一些介绍。
代码实现步骤
public V put(K key, V value) {
// 当插入第一个元素的时候,需要先初始化数组大小
if (table == EMPTY_TABLE) {
// 数组初始化
inflateTable(threshold);
}
// 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到对应的数组下标
int i = indexFor(hash, table.length);
// 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了
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))) { // key -> value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重复的 key,将此 entry 添加到链表中
addEntry(hash, key, value, i);
return null;
}
此段代码为hashmap中1.7的版本,作者已经对里面的一些基本内容做了一些注释,方便大家研究,从代码中可以看出,当新增数据的时候,我们首先会判断table是否为空,table刚才提过就是存储数据的node节点。如果数据为空的情况,我们就进行初始化,接下来我们点进去初始化的方法看一下。
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 保证数组大小一定是 2 的 n 次方。
// new HashMap(519),大小是1024
int capacity = roundUpToPowerOf2(toSize);
// 计算扩容阈值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
这里是初始化为一个容量为16大小的数组,具体的方法自行研究,接下来我们往put方法继续往下看,我们看到最重要的一点,首先新增的key会经过hash计算,计算出其相对应的hash值,然后调用indexFor方法算出其在数组下标的位置,计算出下标的位置后就是进行数组的遍历,这里的数据是一种链表的形式存储,会先判断有没有存在重复的key如果有的话就将其值进行覆盖。其他便是插入新的数据。而在hashmap1.8中这里还多了一步操作当链表的长度大于7的时候,我们将链表转为红黑树的形式。具体大家可以看看以下代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
掌握了1.7的方法 1.8的就不难研究,大家可以参考对比以下,至于数组的扩容,当size达到最大的容量的时候数据会进行扩容,代码如下,这里大概的意思是当扩容的时候我们会重新new 新的数组 扩容的大小为2的n次方。然后将旧的数据迁移到新的数组的地方,注意到这里,这里就会出现线程不安,当一个线程在进行扩容时候 又有另一个 线程进行添加数据操作会发生线程不安全。还有++size操作本身就不具备原子性的操作,因此出现了线程安全的容器ConcurrentHashMap,后面有机会我也会进行介绍,为什么这个容器是线程安全的。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果之前的HashMap已经扩充到最大了,那么就将临界值threshold设置为最大的int值
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);
}
这些基本就是我对hashmap源码阅读解读,笔者是第一次讲解源码,以前无经验,如果有任何问题或者说错的地方欢迎下方评论,或者添加笔者微信讨论。通过最近的阅读源码总结了一套阅读容器的方法给大家分享。
VX:549896196