HashMap源码分析:hash原理和扩容机制

1.HashMap的hash原理

JDK1.8之前HashMap的底层是数组加链表的结构,HashMap通过key的hashcode经过函数扰动后,通过(n-1)&hash来判断当前的元素存放位置。如果存放元素存在冲突,直接覆盖或者只用"拉链法"解决冲突。

JDK1.8之前的hash方法源码如下所示:

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
  • 扰动函数的目的是为了减少hash碰撞。

JDK1.8的HashMap的底层是数组,链表和红黑树的结构。当链表大于阈值(默认为8)时,会将链表转化为红黑树,以减少搜索时间。
JDK1.8的hash方法源码如下所示:

static final int hash(Object key) {
    int h;
    // key.hashCode():返回散列值也就是hashcode
    // ^ :按位异或
    // >>>:无符号右移,忽略符号位,空位都以0补⻬
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • JDK1.8的hash方法中将key的hashcode的高16位与低16位异或,其目的是为了使后续模运算得出的结果(即元素的存放位置)有高低16位的共同特征。

HashMap结构
HashMap取模运算

  • 为了使HashMap的存取高效,并尽量减少碰撞,使得数据分配均匀,需要进行取模运算。由于HashMap的Capacity始终为2的幂次方,故采用“(n-1)&hash”(n代表Capacity大小)代替传统的取模运算,提高运算效率。
2.HashMap的扩容机制

如果创建HashMap时不指定Capacity初始值,HashMap的默认初始化大小为16,之后每次扩充,容量会变为两倍。

HashMap的构造函数和保证总使用2的幂次容量大小的方法如下所示:

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;
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM
}

HashMap的扩容机制—resize()
在内部,JDK1.8之前,HashMap使用一个Entry数组(内部类)保存key、value数据,当一对key、value被加入时,会通过一个hash算法得到数组的下标index。

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。同时为保证HashMap的性能,会进行重散列(rehash)

JDK1.8之前的相关实现如下:

  • put方法:判断key是否已经存在
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 如果key已经存在,则替换value,并返回旧值
    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++;
    // key不存在,则插入新的元素
    addEntry(hash, key, value, i);
    return null;
}
  • addEntry方法:检查容量是否达到阈值threshold,判断是否进行扩容
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);
}
  • resize方法:实现扩容
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ...

    Entry[] newTable = new Entry[newCapacity];
    ...
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
  • transfer方法:扩容时对链表进行处理
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    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() 死循环。

JDK1.8的改进
JDK1.8中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过8的时候,这个链表就将转换成红黑树。

同时,JDK1.8改用Node结构进行k-v存储。Node是HashMap的一个内部类,实现Map.Entry接口,本质仍然是一个k-v映射。

HashMap不会因为多线程put导致死循环(JDK1.8用 head 和 tail 来保证链表的顺序和之前一样;JDK1.7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap。

参考博客:https://www.jianshu.com/p/1e9cf0ac07f4
个人博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值