实现系列-HashMap究竟如何存储

来源:互联网 发布:报刊编辑排版软件 编辑:程序博客网 时间:2024/06/08 13:13
首先要结合数据结构的知识,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();
}

装载因子 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;
    }
}

第一步判断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(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;
    }


这个是entry的定义,可以看出是采用链表链接表示
if(this.size++ >= this.threshold) {
        this.resize(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);
    }
}

这个是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);
        }
    }

}

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(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;
}
var0是hash值,var1是数组长度,
可能存在hash值不同,但是indexFor的值相同的情况














0 0