首先要结合数据结构的知识,hashmap就是hash表,hash表一定会涉及到冲突的处理,结合源码,详细说一下hashmap的具体实现。
首先在我们用hashmap的时候,一般第一步
HashMap<String,String> map=new HashMap<String,String>();
首先看一下这的步骤完成的事情,
public HashMap() {
this.entrySet = null;
this.loadFactor = 0.75F;
this.threshold = 12;
this.table = new HashMap.Entry[ 16];
this.init();
this.entrySet = null;
this.loadFactor = 0.75F;
this.threshold = 12;
this.table = new HashMap.Entry[ 16];
this.init();
}
装载因子 0.75,threshold表示数组现在可容纳最大值,12,初始化了一个16的entry数组,由此可以看到,当数组容量大于12的时候,会进行容量扩充的操作,扩充为二倍。而且,hashmap实际上是以数组形式存储的。
再来看一下put操作。
public V put(K var1, V var2) {
if(var1 == null) {
return this.putForNullKey(var2);
} else {
int var3 = hash(var1.hashCode());
int var4 = indexFor(var3, this.table.length);
for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
if(var5.hash == var3) {
Object var6 = var5.key;
if(var5.key == var1 || var1.equals(var6)) {
Object var7 = var5.value;
var5.value = var2;
var5.recordAccess( this);
return var7;
}
}
}
++ this.modCount;
this.addEntry(var3, var1, var2, var4);
return null;
}
if(var1 == null) {
return this.putForNullKey(var2);
} else {
int var3 = hash(var1.hashCode());
int var4 = indexFor(var3, this.table.length);
for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
if(var5.hash == var3) {
Object var6 = var5.key;
if(var5.key == var1 || var1.equals(var6)) {
Object var7 = var5.value;
var5.value = var2;
var5.recordAccess( this);
return var7;
}
}
}
++ this.modCount;
this.addEntry(var3, var1, var2, var4);
return null;
}
}
第一步判断key是否是null由此可以看到,hashmap是可以插入null的。
如果不为null,那么进行第一步,计算key的hash值,也就是var3,然后根据hash值,算出来这个hash值对应的key在数组中的位置,这里就涉及到一个问题,一样怎么半,这个时候,就是hash冲突的处理了。
如果相等了,说明table[var4]肯定不为null,那么var5不为null,var5的next是否为null不知道,然后循环判断,如果有一个已经是hash值相同,并且key值也相同,那么就可以覆盖这个key的value值了,如果hash值相同,但是不存在put的这个key,那么return var7这个就不会执行。
会执行addEntry这个函数,记住传入的参数,var3 hash值,var 1,var 2要插入元素的key value ,var4 hash值在数组中对应的位置。
void addEntry(
int var1, K var2, V var3,
int var4) {
HashMap.Entry var5 = this.table[var4];
this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
if( this.size++ >= this.threshold) {
this.resize( 2 * this.table.length);
}
HashMap.Entry var5 = this.table[var4];
this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
if( this.size++ >= this.threshold) {
this.resize( 2 * this.table.length);
}
}
首先取出table数组中,var4位置的元素,然后用var1,var2这个键值对新建一个entry,这个entry后面类似于链表一样,连接着var5。这个var5就是以前var4位置的元素。然后用新建的元素充当新的var4,由此可以推断,对于冲突的处理,采用链表法,而且新加入的元素在链表头部。
static class Entry<K, V>
implements java.util.Map.Entry<K, V> {
final K key;
V value;
HashMap.Entry<K, V> next;
final int hash;
Entry( int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
this.value = var3;
this.next = var4;
this.key = var2;
this.hash = var1;
final K key;
V value;
HashMap.Entry<K, V> next;
final int hash;
Entry( int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
this.value = var3;
this.next = var4;
this.key = var2;
this.hash = var1;
}
这个是entry的定义,可以看出是采用链表链接表示
if(
this.size++ >=
this.threshold) {
this.resize(
2 *
this.table.length);
}
这个代码就是之前说的扩容。
问题1:什么时候调用addEntry这个函数
答:当数组table中,var4位置为null的时候和向var4这个位置链接key值不同,但是hash值相同的元素时就会调用,否则不调用
问题2:当容量扩充的时候,get时如何获取到正确位置,因为数组容量已经发生了改变了。
答:
void resize(
int var1) {
HashMap.Entry[] var2 = this.table;
int var3 = var2.length;
if(var3 == 1073741824) {
this.threshold = 2147483647;
} else {
HashMap.Entry[] var4 = new HashMap.Entry[var1];
this.transfer(var4);
this.table = var4;
this.threshold = ( int)(( float)var1 * this.loadFactor);
}
HashMap.Entry[] var2 = this.table;
int var3 = var2.length;
if(var3 == 1073741824) {
this.threshold = 2147483647;
} else {
HashMap.Entry[] var4 = new HashMap.Entry[var1];
this.transfer(var4);
this.table = var4;
this.threshold = ( int)(( float)var1 * this.loadFactor);
}
}
这个是resize函数,首先var3是当前数组容量,var1是扩充以后数组容量,重点是transfer这个函数,
void transfer(HashMap.Entry[] var1) {
HashMap.Entry[] var2 = this.table;
int var3 = var1.length;
for( int var4 = 0; var4 < var2.length; ++var4) {
HashMap.Entry var5 = var2[var4];
if(var5 != null) {
var2[var4] = null;
HashMap.Entry var6;
do {
var6 = var5.next;
int var7 = indexFor(var5.hash, var3);
var5.next = var1[var7];
var1[var7] = var5;
var5 = var6;
} while(var6 != null);
}
}
HashMap.Entry[] var2 = this.table;
int var3 = var1.length;
for( int var4 = 0; var4 < var2.length; ++var4) {
HashMap.Entry var5 = var2[var4];
if(var5 != null) {
var2[var4] = null;
HashMap.Entry var6;
do {
var6 = var5.next;
int var7 = indexFor(var5.hash, var3);
var5.next = var1[var7];
var1[var7] = var5;
var5 = var6;
} while(var6 != null);
}
}
}
transfer这个函数中,var1是扩充的数组,var2是当前数组,一个for循环依次遍历,如果var4位置不为null的话,将var4位置元素给var5,然后var4位置元素设置为null,然后遍历var5以后的元素,具体效果为
如果var4位置的链表形式为1-》2-》3,那么这次以后就会变为3-》2-》1,也就是会反过来,为什么会这样,indexFor(var5.hash, var3) 在确定具体数组位置的时候,var3是扩充以后数组容量,所以会导致重新定位,这个就是为什么要将
var2[var4] =
null;设置为null,以及扩容后也能正确寻找到定位的原因了。
问题3:扩容的时候,条件是
void addEntry(
int var1, K var2, V var3,
int var4) {
HashMap.Entry var5 = this.table[var4];
this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
if( this.size++ >= this.threshold) {
this.resize( 2 * this.table.length);
}
HashMap.Entry var5 = this.table[var4];
this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
if( this.size++ >= this.threshold) {
this.resize( 2 * this.table.length);
}
}
if(
this.size++ >=
this.threshold) 这句话成立会扩容,之前讲过,有两个地方会调用这个函数,table数组中var4位置为null,或者var4不为null,但是添加元素的key值与var4中已经链接的元素indexfor值一样的时候一样,会发生value值的替换。也就是说可能存在这样一种情况,对于容量为16的数组,在数组的某个位置上,可能存在1->2->3->4…->12这种情况,这种情况下,再添加元素的时候
就会引起扩容操作,但是此时数组上只有一个位置有元素,而且链表很长。
这样设计的原因为啥啊?,暂时当作疑问吧,如果限制为size仅仅是var4位置为null,第一次填充的时候,会不会更好一些呢
问题4:如果在var4位置的链表上,1-》2-》3,
1,2,3 三满足什么样的条件?
答:indexFor的值相同,但是hash值可能不同,因为indexFor的函数定义
static int indexFor(
int var0,
int var1) {
return var0 & var1 - 1;
return var0 & var1 - 1;
}
var0是hash值,var1是数组长度,
可能存在hash值不同,但是indexFor的值相同的情况