HashMap?

…衆ロ難τιáo~ 提交于 2020-02-27 19:08:42

HashMap数据结构是什么?
JDK1.7 HashMap由数组+链表组成的,JDK1.8 HashMap由数组+链表+红黑树组成的
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

===============================================================================
key-value是通过什么方式存储进去的?
key使用set集合来存储的,value使用collection来存储的。

===============================================================================
解释hashcode、取余、去重等操作?
在这里插入图片描述
计算hashcode的值:

//这是一个神奇的函数,用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
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);
}

取余:hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置
h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为

/**
* 返回数组下标
*/
static int indexFor(int h, int length) {
return h & (length-1);
}

去重:用equals来去重

===============================================================================

key取余之后它可能全部堆积在某几个key对应的链表上,这样造成该数据结构存储或者低效,怎么解决?

解决哈希冲突主要有三种方法:开放定址法(再散列法),拉链法,再哈希法。HashMap是采用拉链法解决哈希冲突的。
其中开发地址法,就是一旦发生冲突,就往后寻找空的散列地址。缺点是容易产生堆积问题,造成性能过慢。
拉链法是使用数组链表的数据结构,将冲突的对象往链表后面放。不会产生堆积问题,切查找速度较快。java1.8之后对链表做了优化,默认超过8个节点后,链表会转为红黑树。
再哈希就是设置多个hash函数,一旦冲突就是用其他的hash函数再hash,直到没有冲突为止。
建立溢出表:将发生冲突的元素直接放入溢出表中。

===============================================================================

链表为什么要变成红黑树,什么时候链表变成红黑树,什么时候红黑树变回链表?

因为红黑树需要进行左旋,右旋操作, 而单链表不需要,
单链表与红黑树结构对比:
如果元素小于8个,查询成本高,新增成本低
如果元素大于8个,查询成本低,新增成本高

===============================================================================

多个线程并发访问,可能造成容器更新或者操作出现问题,有什么办法解决吗?

在多线程条件下使用HashMap,当两个线程同时操作它,并同对其进行扩容操作,一个线程使用头插法将原来拉链的元素移动到新拉链的头部,此时如果cpu时间片用完,这个线程处于挂起状态。这个时候另一个线程同样这样操作,这个时候可能会产生循环拉链。当下次调用get方法时触发,这个时候便产生了死循环,使cpu使用率达到100%。

===============================================================================
HashMap怎么扩容的?

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

hashmap的resize
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor(加载因子)时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12(扩容的临界值)的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

===============================================================================
HashMap中的方法都什么意思?底层实现?
put方法:
1、如果key为null,存储位置为table[0]或table[0]的冲突链上
2、如果key不为null,调用key1所在类的hashcode()计算key1的哈希值,此时hash值经过 h & (length-1)计算后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。
如果此位置上数据不为空(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已存在的一个或多个数的哈希值:
1、如果key1的哈希值与已经存在的数据的哈希值都不相同,此刻添加成功。
2、如果key1的哈希值和已经存在的某个数据的(key2-value2)哈希值相同,继续比较调用key1所在类的equals(key2)
(1)如果equals返回false,此时key1-value1添加成功
(2)如果equals返回true,使用value1替换value2.

public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(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;
    }

get方法:

当你传递一个key从hashmap总获取value的时候:
对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
key的hashcode()方法被调用,然后计算hash值。
indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。
在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。

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

getOrfault方法:

这个方法同样检查 Map 中的 Key,如果发现 Key 不存在或者对应的值是 null,则返回第二个参数即默认值。
要注意,这个默认值不会放入 Map。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!