java-HashMap实现原理

java中HashMap是如何实现的?
HashMap底层使用的是数组加链表的数据结构实现。

java中HashMap是如何使用的?

首先,得new一个HashMap对象不是?很多人都是用默认的无参构造器来创建HashMap对象的。看一下jdk1.6中的源码

public HashMap() {
//加载因子,默认0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;

    //当HashMap中存储的键值对数超过threshold,会引起扩容
    //默认threshold为 默认数组长度*默认加载因子 即16*0.75
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);

    //创建entry数组,默认值16
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

注释写的很清楚,默认创建一个大小为16的entry数组,当HashMap里存的entry个数达到12,就会发生扩容,这当然会影响HashMap的性能不是?所以,推荐咱们使用HashMap提供的另外一个构造器。

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
调用的是HashMap提供的另一个构造器,传入的加载因子为默认的0.75,看源码

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);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

假如现在我要往HashMap里存12个键值对,那么经过代码段1:

while (capacity < initialCapacity)
capacity <<= 1;

计算得出的capacity为16,然后再经过代码段2:

threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];

创建了一个threshold为12,数组大小为16的entry数组。刚好达到我们的要求。

但是,如果我想要往HashMap里存16个键值对,那么经过代码段1,计算得出的capacity依然为16,然后经过代码段2创建了一个threshold为12,数组大小为16的entry数组。ok那当我往这个HashMap里存入第13个键值对的时候,就会引起扩容(因为threshold =12),扩容操作如果说基数小的话,对性能的影响可能不大,但随着基数的变大,对性能的影响也会随之变大。

所以,推荐在使用这个构造器的时候,我们先计算一下,设你想存x个键值对,那么你应该传入的参数为y。默认加载因子为0.75,那么先用x除以0.75,然后再计算出比x/0.75大的最小的2的幂。

ok,带入计算公式,我要存12个键值对,x=12,则x/0.75=16,因为2^4=16,所以y=16

我要存15个键值对,x=15,x/0.75=20,那么大于20的最小的2的幂为32,y=32,我们传入参数32。这样就不会发生扩容。

当然,理想很丰满,现实很骨感,实际开发中,我们基本上是无法确定我们要存入多少个键值对的,所以,管它呢,咱大概估一个值,传入就行了,但是最好不要使用默认的无参的构造器,原因很简单,假设我们现在要大概要存100个建值对,使用无参构造器,在存入16 * 0.75=12个建值对 的时候会发生第一次扩容,扩容为16 * 2=32,然后在插入32 * 0.75=28个建值对的时候发生第二次扩容,扩容为32 * 2=64,依次类推。但是我们调用new HashMap(100),那么,只会在我们插入第128 * 0.75=96个建值对的时候发生一次扩容。对性能的提升由此可见一斑。

然后,当我们创建好HashMap的实例后,我们会调用它的put方法插入键值对,看源码:

public V put(K key, V value) {
//step 1
if (key == null)
return putForNullKey(value);

    //step 2
    int hash = hash(key.hashCode());

    //step 3
    int i = indexFor(hash, table.length);

    //step 4
    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++;
    //step 5
    addEntry(hash, key, value, i);
    return null;
}

step1:如果传入的key为空,那么返回空键对应的值(HashMap允许一个null键)。

step2:把key的hashcode经过一个hash算法,得到一个hash值。(这个方法咱不讨论,那是数学家研究的事)

step3:根据上一步得到的hash值定位数组的下标。

step4:上一步定位到数组的下标的位置如果有元素,取第一个元素,判断他们的hash值是否相同,如果相同,再判断两个对象是否相等。如果hash值不同,或hash值相同,却不相等,判断第二个元素。直到判断完所有的元素。

step5:把当前元素作为数组下标位置的第一个元素,把之前的元素设为新插入元素的next节点,判断是否超过threshold,超过则扩容(addEntry逻辑说明)。

这里需要注意的是step3中,根据hash值定位数组下标后,这个下标处的元素的hash值是不一定相等的。为什么?看step3处的方法的源码:

static int indexFor(int h, int length) {
return h & (length-1);
}
ok,假如传进来的hash值为1,那么返回的下标是1 & 15

java中int类型是32位那么 1 & 15 如下:

0000 0000 0000 0000 0000 0000 0000 0001 -1的二进制表示

0000 0000 0000 0000 0000 0000 0000 1111 -15的二进制表示

按位与操作只有两个数都是1才会返回1,否则返回0。

那么显然,1 & 15 得1,即返回的数组下标为1。

咱再来看看如果传进来的hash值是17,会怎样。

ok,若传进来的hash值为17,则返回 17 & 15

0000 0000 0000 0000 0000 0000 0001 0001 -17的二进制表示

0000 0000 0000 0000 0000 0000 0000 1111 -15的二进制表示

对的整整齐齐,明明白白,一眼就能看出17 & 15 = 1

综上,不同的hash值通过下标是可以定位到相同的数组位桶的。

这里的这个定位算法和数组的长度也是有关系的,为什么HashMap的数组长度是2的幂?

咱假设这里不是二的幂,看看会出现啥情况。

若数组的长度是10,indexFor方法返回的是 h & 9 则

0000 0000 0000 0000 0000 0000 0000 1001 -9的二进制表示

看到最后4位,中间的两个0了么?无论你传进来的hash值是多少,中间那两个0相与,永远返回0。这样一来,就大大的增加了hash碰撞的概率。

所以这时我们再来看看构造器中这一段代码:

while (capacity < initialCapacity)
capacity <<= 1;

它能保证初始化的数组的大小一定是2的幂。以便在定位数组位桶的下标时使用。

最后,我们使用get方法,从HashMap中找到我们想要的键值对。来看看get方法的源码:

public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
这个就不用多说了吧。先hash,再定位,再比较hash值,再equals。。。

HashMap使用的一些建议
一般而言,HashMap的key是要用类似String、Integer这样的不可变类的。夺西碟?

咱们看看String类的hashCode方法:

//成员变量hash,初始值为0.
private int hash;

public int hashCode() {
int h = hash;
int len = count;
//如果h==0代表第一次调用此方法,调用完之后,把计算出来的值付给成员变量hash
//第二次调用此方法时,h == 0 返回 false,直接return h。
if (h == 0 && len > 0) {
int off = offset;
char val[] = value;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

使用String当做key,只会在第一次调用hashCode方法时,计算出hash值,以后的调用直接使用已缓存的hash值即可。这样设计,又能大大的提升HashMap的性能。

所以,推荐使用这种不可变类。

当然,如果你想用自定义的类当做key,那么,请参考String类的设计,而且,这个你自定义的类必须是不可变得,至少参与hashCode计算的字段是不可变的,不然,会出现存进去的键值对和取出来的键值对不一致。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值