HashMap

1、HashMap原理

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap 的基本组成单元HashMapEntry,如下,HashMap的静态内部类

static class HashMapEntry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    HashMapEntry<K,V> next;//存储指向下一个HashMapEntry的引用,单链表结构
    int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

    HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    //...
}

其结构图大致如下:
这里写图片描述
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;
如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。

2、HashMap初始化

public HashMap(int initialCapacity, float loadFactor) {
    //检验initialCapacity、loadFactor
    ……
    threshold = initialCapacity;
    init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
}

initialCapacity:初始化申请空间,默认是4,即数组内部长度是2^4 = 16
loadFactor:扩容因子,默认0.75
threshold:下次扩容时的临界值,在inflateTable的时候确认,等于capacity * loadFactor

其他两个构造最终都是调用上面这个,在构造中,并没有初始化数组table(那个传入一个Map集合的构造除外),而是在执行put操作的时候才真正构建table数组。

    public V put(K key, V value) {
    //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(2^4=16)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
   //如果key为null,存储位置为table[0]或table[0]的冲突链上
    if (key == null)
        return putForNullKey(value);
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//对key的hashcode进一步计算,确保散列均匀
    int i = indexFor(hash, table.length);//获取在table中的实际位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
        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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
    addEntry(hash, key, value, i);//新增一个entry
    return null;
} 
public static int singleWordWangJenkinsHash(Object k) {
    int h = k.hashCode();
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

这里,在put的时候,调用inflateTable来初始化table

private void inflateTable(int toSize) {
    // capacity一定是2的次幂,即保证数组长度一定是2的次幂
    int capacity = roundUpToPowerOf2(toSize);
    float thresholdFloat = capacity * loadFactor;
    if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
        thresholdFloat = MAXIMUM_CAPACITY + 1;
    }
    threshold = (int) thresholdFloat;// 下次扩容临界值
    table = new HashMapEntry[capacity];//初始化哈希表
}

那么为什么HashMap的数组长度一定是2的次幂?
put()中,通过transfer()获取元素在数组中的索引,index是通过key的hash值和length-1通过位运算得到的,除非hash值一致,得到的index是不一样的,而且能更加均匀的在table中存放,大概率降低冲突。
而且,在resize()扩容的时候,数组长度发生变化,存储的位置index = h & (length-1)可能会变化,

void resize(int newCapacity) {
    HashMapEntry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    HashMapEntry[] newTable = new HashMapEntry[newCapacity];//创建一张新表
    transfer(newTable);//将旧表的数据循环遍历,导入新表
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (HashMapEntry<K,V> e : table) {
        while(null != e) {
            HashMapEntry<K,V> next = e.next;
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

transfer()里面,将老数组中的数据逐个链表地遍历,扔到新的扩容后的数组中,通过indexFor()重新获取元素在table中的索引位置。正因为数组的长度是2的次幂,所以新表扩容后,在通过位运算获取index时,length-1的低位全为1,高位为0,因此除非hash值在高位也为0,否则旧表与新表的index保持一致,保证了之前旧表散列良好的数据不会因为扩容发生冲突。

modCount如何保证线程安全,如何能安全的运用HasMap
modCount的意思就是修改次数,在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。
安全使用:
1、使用线程安全的ConcurrentHashMap或HashTable,它就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制
2、Collections.synchronizedMap将HashMap包装起来

Map m = Collections.synchronizedMap(new HashMap());
...
Set s = m.keySet();  //不需要加锁
...
synchronized(m) {  // 对Map的对象m加锁
Iterator i = s.iterator(); // 必须加锁的模块
    while (i.hasNext())
        foo(i.next());
}

3、Hash冲突

两个不同的元素,通过哈希函数得出的实际存储地址相同,即Hash冲突。
解决方法:
开放定址法(再散列法):当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
再哈希法:同时构造多个不同的哈希函数,当第一个函数生成当hash值冲突的时候,计算第二个哈希函数,直到不冲突为止。这种方法不易产生聚集,但增加了计算时间。
链地址法:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

HashMap即是采用了链地址法,也就是数组+单链表的方式

参考文章

1、HashMap实现原理及源码分析
2、HashMap的实现原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值