1.写在前面
前面的Redis的系列的博客已经结束了,今天我打算讲一下HashMap的源码,今天我打算讲一下1.7的HashMap的源码,因为1.7和1.8的源码的变化还是挺大,所以今天我打算讲一下1.7的HashMap的源码,因为1.8的就已经完全不一样了。
2.Map接口
因为HashMap的源码是实现Map的接口,我们平时也都用Map这个接口来接收,所以我们直接看下Map这个接口有哪些方法,具体的如下:
public interface Map<K,V> {
//获取Map的长度
int size();
//map的长度是否为空
boolean isEmpty();
//是否包含键
boolean containsKey(Object key);
//是否包含对应的值
boolean containsValue(Object value);
//根据对应的键获取对应的值
V get(Object key);
//设置对应的键和值
V put(K key, V value);
//根据键删除对应的值
V remove(Object key);
//将map设置到map中去
void putAll(Map<? extends K, ? extends V> m);
//清空map
void clear();
//获取对应的键的set
Set<K> keySet();
//获取对应值的set
Collection<V> values();
//获取对应的键值对的set
Set<Map.Entry<K, V>> entrySet();
//Map中的Entry
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
}
Entry的结构
对于Map中存储的是Entry,所以我们要看下Entry的结构,具体的代码如下:
static class Entry<K,V> implements Map.Entry<K,V> {
//键
final K key;
//值
V value;
//键值对
Entry<K,V> next;
//hash值
int hash;
//构造函数
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//获取键
public final K getKey() {
return key;
}
//获取值
public final V getValue() {
return value;
}
//设置值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//比较两个Entry是否相等
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//计算hashcode
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
}
初始化方法
上面差不多就是Map中的每一个Entry的差不多存的就是一个键值对,然后我们先来看下对应的HashMap的构造方法,对应的源码如下:
//initialCapacity = 16
//loadFactor = 0.75
//threshold = initialCapacity
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();
}
上面的代码主要赋值了三个值,一个initialCapacity
初始化的值(16),loadFactor
负载因子(0.75),threshold的值等于16。
put方法
这个时候我们再来看下put
的方法,具体的代码如下:
public V put(K key, V value) {
//第一次的时候一定是成立
if (table == EMPTY_TABLE) {
//threshold = 16
inflateTable(threshold);
}
//如果key的值为空,直接put一个空的Key
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
//初始化数组了
private void inflateTable(int toSize) {
//15->16 四舍五入容量成为2次方幂
int capacity = roundUpToPowerOf2(toSize);
//容量*0.75
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建指定容量的Entry(这儿的容量是四舍五入的2次方幂)
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
final boolean initHashSeedAsNeeded(int capacity) {
//默认的情况下 currentAltHashing 是false
boolean currentAltHashing = hashSeed != 0;
//第一项判断jvm是否启动了 true
//capacity=16第一次的情况下,
//Holder.ALTERNATIVE_HASHING_THRESHOLD是Integer的最大值,如果传了值就是指定的值
//第二项判断是false
//useAltHashing就是false
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//异或成为false
boolean switching = currentAltHashing ^ useAltHashing;
//不进这个循环
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
上面的代码先初始化Entry数组的类,然后设置threshold的值,是为初始化的值*0.75,然后初始化指定容量的Entry,然后设置hashSeed的值,默认的情况的下0,一般的情况下是不会改变,可以具体看下我上面的注释。然后如果key为空,这个时候会调用putForNullKey(value);
方法,具体的代码如下:
//设置默认的空键的值
private V putForNullKey(V value) {
//遍历所有的Entry,设置好对应的Entry
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++;
//第一次为空的情况下,添加对应Entry
addEntry(0, null, value, 0);
return null;
}
上面的代码就是遍历所有的table的数组,然后设置table的值,一般的情况下,第一次进来的时候是空的,然后就会调用addEntry(0, null, value, 0);
具体的代码如下:
// 0 null value 0
void addEntry(int hash, K key, V value, int bucketIndex) {
//长度大于初始化长度*0.75,同时这项不等于null
//会调用扩容 这个时候不会进入
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建Entry的方法
createEntry(hash, key, value, bucketIndex);
}
上面的代码的是先判断是否需要扩容,扩容的条件:**长度大于初始化长度*0.75,同时插入的下标在数组不为null。**然后创建Entry,具体的代码如下:
//创建Entry
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
创建好的Entry,然后把size的值++,具体的我们来看下扩容的机制,具体的代码如下:
//扩容的长度是原来的长度*2
void resize(int newCapacity) {
//先将原来的值赋值给oldTable
Entry[] oldTable = table;
//老的长度
int oldCapacity = oldTable.length;
//如果的长度等于最大的长度,就直接用最新的
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根据新的长度创建好的newTable
Entry[] newTable = new Entry[newCapacity];
//initHashSeedAsNeeded(newCapacity)这个返回值为false
//转移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//计算容量*负载因子
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
上面的代码就是扩容的容量是原来的2倍,然后主要是调用转移的方法transfer(newTable, initHashSeedAsNeeded(newCapacity));
,具体的代码如下:
//rehash = false
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历原来的数组table
for (Entry<K,V> e : table) {
//遍历数组下面的链表
while(null != e) {
Entry<K,V> next = e.next;
//不需要重新rehash
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;
}
}
}
这块的扩容和转移的代码后面我会画一个流程的图,给你们看看,这个时候我继续看put方法的后面的代码,具体的如下:
public V put(K key, V value) {
//第一次的时候一定是成立
if (table == EMPTY_TABLE) {
//threshold = 16
inflateTable(threshold);
}
//如果key的值为空,直接put一个空的Key
if (key == null)
return putForNullKey(value);
//根据key 计算hash的值
int hash = hash(key);
//计算出来位置
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++;
//添加Entry,和刚才的putForNullKey(value);一样
addEntry(hash, key, value, i);
return null;
}
讲到这个时候,put的方法差不多讲完了,差不多的逻辑,就是判断是否是空的,然后如果是空的,初始化这个table,如果这个键是null,然后就put空的null的键,这个时候也是插入数据,先判断需不需要扩容,如果需要扩容,就先扩容,然后再添加。再然后是计算hash的值,以及计算出来的位置,然后遍历这个数组的位置的链表,找到相同的替换,如果没有,直接添加这个Entry,这个时候也是判断是不是需要扩容,如果需要扩容,就先扩容,如果再添加。这儿后面会画个流程图。
get方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
如果这个key为空的话,直接调用getForNullKey()
方法,具体的代码如下:
private V getForNullKey() {
//如果长度为0,那么就直接返回null
if (size == 0) {
return null;
}
//遍历数组0号下标的链表,找的键为null的值,直接返回对应的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
如果长度为0,然后直接返回null,然后遍历数组的0号下标的链表,然后如果找到对应的key为空,就直接返回这个值。继续看getEntry(key)的方法,具体的代码如下:
final Entry<K,V> getEntry(Object key) {
//size的长度为空,直接返回null
if (size == 0) {
return null;
}
//计算hash的值
int hash = (key == null) ? 0 : hash(key);
//遍历这个位置的链表,找到就直接返回这个entry
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
f (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
get的方法还是比较简单,具体的流程可以看后面的图。
remove方法
具体的代码如下:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
主要调用的是removeEntryForKey(key)
方法,具体的代码如下:
final Entry<K,V> removeEntryForKey(Object key) {
//如果size的长度等于0,直接返回null
if (size == 0) {
return null;
}
//计算好hash的值
int hash = (key == null) ? 0 : hash(key);
//根据这个hash计算出位置
int i = indexFor(hash, table.length);
//指向头结点
Entry<K,V> prev = table[i];
//e表示头结点
Entry<K,V> e = prev;
//遍历对应的链表,找到相等的key,然后删除
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
上面的代码就是大概的删除的流程,后面会画一个具体的流程图,对熟悉链表的操作的一些小伙伴,这部分的代码应该是比较简单。我这儿就不细说了,后面直接上流程图。
其他的方法就暂时不介绍了,因为其他的方法都是依赖这几个方法,也就是维护几个变量。
3.具体的流程图
put的流程
前面常用的方法就直接介绍完了,大概的源码已经介绍完了,具体的流程的如下,我们先来看下具体的流程图,put的流程图如下:
看完了put的流程,我们现在看看get的流程。
get的流程
我们还需要看下remove的流程,具体的如下
remove的流程
还有其他的链表的操作的方法,我这儿就不做过多的介绍了,因为这是数据结构和算法方面的知识,记住1.7的HashMap的源码的链表是头插法的方式。
4.写在最后
本篇博客大概的介绍了下HashMap(1.7)的源码,后面会介绍HashMap(1.8)的源码,至于头插法和尾插法操作链表的算法,后面的博客会介绍。