HashMap的工作原理

面试提问


“你用过HashMap吗?” “什么是HashMap?你为什么用到它?”

答:

HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等

“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”

答:

HashMap是基于“散列表”的原理实现的,内部维护了一个数组来存储Entry对象。每个Entry对象内就是一个key/value结构。
当我们往集合中put一个键值时,内部会先对键调用HashCode()方法,返回的hashCode用于找到数组(俗称:桶)位置来储存Entry对象。
当调用get()方法时,通过键对象hashcode值找到对象位置,然后调用equals()方法找到正确的键值对,最后返回值对象。

注:equals一样时,hashcode也一样,hashcode一样,equale不一定一样


“当两个对象的hashcode相同会发生什么?”

答:

如果两个不同的JKey计算出的hashcode是一样的,就会发生两个不同的key都对应到数组中同一个位置的情况,也就是所谓的哈希冲突。

HashMap处理哈 希冲突的方法是拉链法,也就是说数组中每个位置保存的实际是一个Entry链表,
链表中每个Entry都拥有指向链表中后一个Entry的引用。在发生哈希冲突时,将冲突的Entry追加至链表的头部。 ---

“如果两个键的hashcode相同,你如何获取值对象?”

答:

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,
找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象.

“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”

答:

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,
将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。
这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

在多线程的时候,重新调整HashMap的大小时会出现什么情况?

答:

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,
它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候
,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。
如果条件竞争发生了,那么就【死循环】了。

为什么String, Interger这样的wrapper类适合作为键?

答:

因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点
同时还减少了碰撞的机率。

HashMap方法源码解析

put(key, value):

public V put(K key, V value) {
    //当key为null,调用putForNullKey方法,保存null于table第一个位置中,这是HashMap允许为null的原因
    if (key == null)
        return putForNullKey(value);
    //计算key的hash值
    int hash = hash(key.hashCode());
    //计算key hash 值在 table 数组中的位置
    int i =  indexFor(hash, table.length)
    //从i出开始迭代 e,找到 key 保存的位置
    for (Entry<K, V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判断该条链上是否有hash值相同的(key相同)
        //若存在相同,则直接覆盖value,返回旧value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;    //旧值 = 新值
            e.value = value;
            e.recordAccess(this);
            return oldValue;     //返回旧值
        }
    }
    //修改次数增加1
    modCount++;
    //将key、value添加至i位置处
    addEntry(hash, key, value, i);
    return null;
}

HashMap通过键的hashCode来快速的存取元素。当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。 先说说大概的过程:当我们调用put存值时,HashMap首先会获取key的哈希值,通过哈希值快速找到某个存放位置,这个位置可以被称之为bucketIndex。 对于一个key,如果hashCode不同,equals一定为false,如果hashCode相同,equals不一定为true。

所以理论上,hashCode可能存在冲突的情况,也叫发生了碰撞,当碰撞发生时,计算出的bucketIndex也是相同的,这时会取到bucketIndex位置已存储的元素,最终通过equals来比较,equals方法就是哈希码碰撞时才会执行的方法,所以说HashMap很少会用到equals。HashMap通过hashCode和equals最终判断出K是否已存在,如果已存在,则使用新V值替换旧V值,并返回旧V值,如果不存在 ,则存放新的键值对<K, V>到bucketIndex位置。

get(key):

public V get(Object key) {
    // 若为null,调用getForNullKey方法返回相对应的value
    if (key == null)
        return getForNullKey();
    // 根据该 key 的 hashCode 值计算它的 hash 码
    int hash = hash(key.hashCode());
    // 取出 table 数组中指定索引处的值
    for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
        Object k;
        //若搜索的key与查找的key相同,则返回相对应的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

在这里能够根据key快速的取到value,除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系,在前面就提到过,HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理的,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象

概括

HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

因为HashMap的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦