HashMap---1.7源码阅读

JDK1.7

注意点:1、缺省值length为空,初始大小默认为16.每次扩容为原来的2倍,负载因子为0.75.扩容阈值为16*0.75=12,是超过12的时候进行扩容

    2、数组+链表。冲突时,头插法。先扩容,再插入(1.8先插入再扩容)

    3、key为null,且数组不为空,则放在table[0]的位置

    4、hashcode&(length-1)为数组下标。扩容后,元素要么在原位置,要么在原位置+原length的位置

    5、hashMap大小必是2的幂次方数。目的是尽量较少碰撞,也就是要尽量把数据分配均匀

 

1、数组   存放元素的是Entry<K,V>[] table数组,初始为空

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE

  无参构造方法,默认空,插入元素的时候初始化为16,负载因子为0.75

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 

2、put方法

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {    //数组为空,则先初始化
            inflateTable(threshold);
        }
        if (key == null)  //key为null,则方法
            return putForNullKey(value);
        int hash = hash(key);    //计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode
        int i = indexFor(hash, table.length);  //与运算,得到放在table的数组下表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  //该数组位置无参数则直接放,有参则遍历链表,key的hashcode相等且equals相同则覆盖,
            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 V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {    //已有key为null的值则覆盖
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);  //无key为null,则放在0的位置
return null;
}
final int hash(Object k) {    //计算key的hashcode值
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {    //利用hashcode和数组长度-1“与”运算得到数组位置
return h & (length-1);
}

void addEntry(int hash, K key, V value, int bucketIndex) {//插入方法
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}

createEntry(hash, key, value, bucketIndex);
}

数组为空,则先初始化

key为null则判断,已有key为null则覆盖,无key为null则放在table数组的0位置

计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode

hashcode和数组长度进行与运算h & (length-1),得到应放的数组下标

该数组位置无参数,则直接放

有参则遍历链表,key的hashcode相等且equals相同则覆盖,无相同则使用头插法,插到链表头部

插入前先判断,是否需要扩容,如果要,则先扩容,在放元素

 

注:先扩容,再插入。头插法。因为是头插法,多线程的情况下会导致死循环。头插法速度比尾插法快

 

3、get方法

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {      //数组下表为0的元素
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}

int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}

 先判断key是否为null,是则判断数组是否为空,空则返回null.非空则返回table[0]

  key不是null,先判断数组是否为空,是则返回null.

不是则先计算key的hash值,在找到通过hash&(length-1)数组下标找到对应链表

遍历链表,hashcode相同并且keyequals相同时,找到元素,返回value

 

4、扩容

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        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);
    }

每次扩容为原来的2倍

遍历数组和链表,重新计算元素的hashcode和位置,依次放入元素。

补充:扩容后的元素,要么在原位,要么在原位+原数组长度=新位置。1.7是每次都重新计算,1.8做了优化,直接计算高位是否为1,为1则原位+原数组长度=新位置,为0则在原位置

头插法,元素倒叙

 

5、hashMap的中table数组位置的计算方法

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

问:为什么要进行^运算和>>运算呢?因为要让hashcode的高位数参数计算,减少hash碰撞

举例:length=16      length-1=15 : 0000 1111

h:    :1111  0011

length-1:0000 1111

               0000 0011   结果为3,放在数组【3】的位置

再为进行>>和^运算时,hashcode只有地位进行了运算,高位是什么不影响结果,因此要通过5次异或运算和4次位移运算使hashcode尽量分散,减少碰撞

 

6、hashMap的大小,始终为2的幂次方,无论带参设置多少,都取大于该值得最小2的幂次方

 private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

 问:为什么要保证为2的幂次方?

因为hashcode&(length-1)=数组位置,只有当length为2的幂次方时,length-1才能是低位为都为1的二进制,进行与运算才不会数组越界。因为都为1111才有与运算的意义,如果都为0,则无论如何都为0,会加剧hashcode的碰撞

16:0001 0000

15:0000 1111   

问:为什么扩容后的数组元素,要么在原位置,要么为原位置+原来length的位置?

length*2-1   实质上是二进制的高位从0变成1

15: 0000 1111

31:0001 1111    

进行与运算时,要么是1,要么是0。是1,则原位置+原来length的位置。是0则位置不变

 

 

7、多线程下,扩容变成死循环的问题   参考:https://www.cnblogs.com/devilwind/p/8044291.html

根本原因:头插法,会使原有元素倒叙

线程1:扩容后,遍历元素前停止

线程2:扩容后,遍历元素,顺利完成

 

  

posted on 2020-07-02 20:47  潮流教父孙笑川  阅读(92)  评论(0)    收藏  举报

导航