HashMap的数据结构

目录

一、数据结构

如何存储

 如何应对冲突(链表+红黑树)

二、扩容

第一次扩容

 后续扩容

 加载因子(填充因子)


HashMap是Map接口的一个实现类,hashMap中,将键和值封装成一个entry对象,其中键唯一,而值可以重复,二者都允许为null,此时存储在该数组下表为0 的位置。集合内部无序。

一、数据结构

在JDK1.8以前使用数组+链表存储

在 JDK1.8以后使用数组+链表+红黑树存储

如何存储

hashMap内部使用Node<K,V>类型的数组存储数据,在如下的源代码中,我们发现,Node<K,V>是Map.Entry<K,V>的实现类,因此我们所说的键和值被封装成entry对象实际上是被封装成了Node对象,该类中包含三个属性分别是key的hashcode,key,value以及next

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//需要根据该hash值确认该对象在数组中位置
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

 如何应对冲突(链表+红黑树)

当一个对象被put进map集合时,会执行put方法,在put方法中返回的putVal会调用hash(key)方法,该方法的目的是,获取键的hashcode并取其高16位,然后返回。

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

此时的hashcode被用来计算该对象在Node数组中的位置

1.在JDK1.8以前

        用数组的长度与该hashcode取余,得出位置

2.在JDK1.8以后

        由于取余的运算符%效率较低,因此在1.8后使用(n - 1) & hash 确定位置,此处的n为数组长度,n必须是2的n次幂。

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        

在计算后存入数组

 //hashcode未冲突,new新的节点将键值对存入数组对应位置
if ((p = tab[i = (n - 1) & hash]) == null)
     tab[i] = newNode(hash, key, value, null);
        

但是在多次put后,hashcode会产生冲突的可能,一般来说会有两种冲突

冲突1:

        该类型的hashcode相同的概率较高,为了降低该种概率,会将hashcode再次计算得到最终的hashcode

冲突2:

        由于hashcode是一个int类型的数据,而int类型的最大值是2的32次方,因此不可能每个对象都有相同的hashcode

因此对于相同的hashcode,在向map中存入数据前,会通过比较二者的key是否一致。若一致,将旧的value更新。若不一致,则需要重新存储,但依然是在该位置上,不过是以链表的形式存储,该链表为单向链表,使用node<K,V>对象的next属性指向新的节点。

在经历多次put后,该数组的某个节点可能会产生一条规模较大的链表,此时会产生新的问题,即查找getKey(key)方法。从该方法的源代码中,我们可以观察到,在根据key查找时,会先计算该key的hashcode,找到对应的位置,然后从该位置开始遍历链表,查找指定元素。这个过程是非常繁复的。

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

 因此当链表规模过时,为提了提高查询效率,链表会转换成一颗有序的树。当计算完hashcode后,与树种节点的hashcode进行比较,大于则向下比较左子树,小于比较右子树,以此提高查找效率。

二、扩容

在ArrayList数组扩容中,我们说到数组的扩容方式有三种,但对于HashMap来说并不适用。

第一次扩容

数组Node<K,V>数组被初始化后,长度为0,第一次put对象时,扩容发生,此时,数组table的长度为0,调用resize()方法,为数组开辟16个位置

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        

 后续扩容

在向数组中添加内容时,若hashcode重复,会在同一位置产生链表,当该链表的长度为8且数组长度小于64时,调用resize()方法咱找原数组大小的2倍进行扩容,扩容后,数组内的元素重新计算位置

 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
     resize();

若大于64则将链表转换成树

 加载因子(填充因子)

用于指定数组的实际使用率,当hashMap中的元素个数超过 数组大小*加载因子时,原数组进行扩容

1.使用无参构造初始化==>public HashMap()

        数组默认长度为16,加载因子为0.75,即当数组元素个数大于12时,进行扩容

2.使用有参构造初始化==>public HashMap(int initCapacity,float loadFactor)

        指定数组长度(必须是2的n次幂),指定加载因子

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gurean

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值