HashMap实现原理(jdk1.7),源码分析

Posted songjilong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap实现原理(jdk1.7),源码分析相关的知识,希望对你有一定的参考价值。

HashMap实现原理(jdk1.7),源码分析

? HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数组就是HashMap的主干。

一、几大常量

//默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; 

//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; 

//空的哈希表
static final Entry<?,?>[] EMPTY_TABLE = {};  

//实际使用的哈希表,存储数据的数组,Entry类型,每个键值对都是一个Entry
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//数组
transient int size; 

//阈值
int threshold; 

//负载因子
final float loadFactor;  

//修改次数,用于多线程问题
transient int modCount;

//使用替代哈希的默认阀值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

//随机的哈希种子, 有助于减少哈希碰撞的次数
transient int hashSeed = 0;

二、构造器

//以两参构造器为例
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();
}

三、put方法

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        //1.如果hash表为空,则根据阈值扩展
        inflateTable(threshold);
    }
    if (key == null)
        //2.键为空的处理方式
        return putForNullKey(value);
    //3.计算key的哈希值
    int hash = haSsh(key);
    //4.根据上一步的哈希值得到其再数组中的位置i
    int i = indexFor(hash, table.length);
    //5.如果i上有元素,则对当前位置的链表进行遍历(HashMap是由数组加链表构成的,一个位置上可能不止有一个元素)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //5.1. 查看该链表是否有键相同的元素,如果有,就以新值替换旧值,并返回旧值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //6.修改次数+1
    modCount++;
    //7.使用头插法插入Entry对象
    addEntry(hash, key, value, i);
    return null;
}

put内的方法深入分析:

1. inflateTable(threshold);

private void inflateTable(int toSize) {
    //算出一个大于等于toSize的 2的次方数 作为哈希表的容量
    int capacity = roundUpToPowerOf2(toSize);
    //新的阈值,大小为 容量*负载因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

2. putForNullKey(value);

private V putForNullKey(V value) {
    //这里可以看到,key为null时和正常的put没什么区别,只不过是直接以数组第一个位置作为插入点
    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++;
    addEntry(0, null, value, 0);
    return null;
}

3. indexFor(hash, table.length);

//使算得的hash值与 数组长度-1 进行"与"运算
static int indexFor(int h, int length) {
    //官方源码注释说的很清楚了,长度必须是非零的2次方数
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
/*
那么为什么一定要是2的次方数呢
假设length=16,那么15的二进制则为0000 1111(以8位表示),
如果h = xxxx 0101, h = xxxx 1111,下面进行"与"运算

    0000 1111           0000 1111           0000 1111
    xxxx 0101           xxxx 1111           xxxx xxxx
& ——————————————    & ——————————————    & ——————————————
    0000 0101           0000 1111           0000 xxxx
    
根据上述结果我们可以得出这样的结论:任何一个hash值 & (length-1) 的结果只与hash的后n位有关,n取决于length是2的几次幂,这样,运算出来的结果一定在0 ~ (length-1)这个范围之内
    
*/

4. addEntry(hash, key, value, i);

void addEntry(int hash, K key, V value, int bucketIndex) {
    //扩容操作,当hash表元素个数大于等于阈值了,并且要插入的位置已经有值了,才进行扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容位原来容量的2倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        //计算元素在数组插入的位置
        bucketIndex = indexFor(hash, table.length);
    }
    //创建新的Entry对象放到对应的位置,使用的是头插法
    createEntry(hash, key, value, bucketIndex);
}

5. resize(2 * table.length);

//这是addEntry中的方法,用于对hash表进行扩容,其实就是新建一个长度为newCapaciy的数组,然后把旧数组的元素放到新数组
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);
}

//transfer分析
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍历原hash表,e指向当前元素,next指向e的下一个元素
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            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;
        }
    }
}

四、get方法

public V get(Object key) {
    if (key == null)
        //key为空的情况,和上述的putForNullKey相同,也是从数组第一个位置寻找
        return getForNullKey();
    //遍历数组,根据key的hash值找到数组的位置,然后遍历链表
    Entry<K,V> entry = getEntry(key);
    //返回key对应的值
    return null == entry ? null : entry.getValue();
}

以上是关于HashMap实现原理(jdk1.7),源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java中HashMap底层实现原理(JDK1.8)源码分析

Java中HashMap底层实现原理(JDK1.8)源码分析

JDK1.7中HashMap底层实现原理

HashMap和ConcurrentHashMap实现原理及源码分析

JDK1.7 HashMap 源码分析

JDK1.7&1.8源码对比分析集合HashMap