HashMap集合原理

HashMap集合原理

0、问题
  • hashmap的结构是什么?(数组 + 链表 + 红黑树)
  • 为什么要进行扩容?何时扩容?每次扩容多少?(均匀散列,提高效率。默认每次扩容为原来的一倍)
  • key值是如何定位在table数组中的索引位置的?(key的 hashcode & (table数组长度 - 1)
  • 如何得到的散列值?如何尽可能确保key均匀且高效的散列到数组中? hash & (table数组长度 - 1)
  • 为什么不用hashcode取模运算取得hash值?而是使用hashcode & (length - 1)? 取模运算开销大
  • 为什么hashMap集合的table容量值建议设置为2的n次方?(散列均匀、碰撞几率小,与运算有关系)
  • 自定义对象作为key时要注意什么?(覆写hashcode和equals)
  • map的put( key , value ) 和get( key )原理?
1、hashmap的结构

在这里插入图片描述

hashmap结构由数组和链表组成(jdk8以后是数组+链表+红黑树)。

在jdk8以上,每当链表长度大于8时自动将链表结构转换为红黑树(提高搜索效率)。

2、扩容机制

扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能 。阿里开发手册也建议:在new HashMap<>(size)的时候尽量指定初始大小。

1、三个构造函数:

HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

2、为什么要扩容?

当map集合中数据越来越多的时候, 碰撞的几率也就越来越高 ,链表长度也会变得很长,每次插入或获取一个数据需要遍历链表,当链表非常长的时候遍历链表是非常耗时的,所以需要扩充容量来提高效率。

3、何时扩容?扩容为多大?

默认是当table表中空间使用程度达到 HashMap() 容量 * 加载因子(默认0.75) 的时候自动进行扩容。

每次扩容大小为原来的一倍。

4、如何扩容?

当table表中空间使用程度达到 HashMap() 容量 * 加载因子(默认0.75) 的时候进行自动扩容,由于数组没有办法直接增大容量,所以首先要新建一个数组(容量为原来容量的2倍),然后然后重新计算每个元素在新数组中的位置(这个操作非常耗时,但是由于底层一系列算法实现,将效率已经变得很高了,扩容后一条数据要么在原位置、要么是在原位置再移动2次幂的位置

5、扩容源码:

void resize(int newCapacity) {   //传入新的容量  
    Entry[] oldTable = table;    //引用扩容前的Entry数组  
    int oldCapacity = oldTable.length; 
    
    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了  
        threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了 
        return;  
    }  
  
    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组  
    transfer(newTable);                         //!!将数据转移到新的Entry数组里  
    table = newTable;                           //HashMap的table属性引用新的Entry数组  
    threshold = (int) (newCapacity * loadFactor);//修改阈值  
}  

数组元素拷贝源码:

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了旧的Entry数组  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
        if (e != null) {  
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
                e.next = newTable[i]; //标记[1]  
                newTable[i] = e;      //将元素放在数组上  
                e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  
}  
3、数据插入和取得流程

a)、插入数据:

  1. 判断是否为空,为空则调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因

  2. 当向hashmap中插入一条数据Node时(key-value),先获取key的hashcode,然后通过运算hashCode & (table数组长度 - 1)得到一个hash值,通过该hash值去table数组中找对应的位置。

  3. 检查该位置上是否已经有元素,若没有,则直接将Node放在该位置。

  4. 若该位置上已有元素,则遍历链表,使用key的hash值与equals()方法逐一去与链表的节点比较。

    ​ a)、若有key值相同的节点,则直接替换value值为新值。

    ​ b)、若没有key值相同的节点,则直接将数据插在链表头部。

** 注意 :**在jdk8以后,插入链表前首先要对链表长度进行检查,检查是否大于8,若大于8则自动转换为红黑树结构

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());                  ------(1)
        //计算key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //从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;
    }

b)、取得:

  1. hashmap.get(“key”)方法在map集合中通过key获取数据,首先通过hash运算取得key的hashcode,
  2. 然后将hashcode & (length - 1)得到hash散列值,通过该值定位到table数组中的具体索引位置。
  3. 遍历链表,通过hashcode和equals比较key是否相同。若相同,则返回对应的value值,否则返回null
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;
    }
4、其他

1、如何得到key在table数组中该存放在哪个位置?

通过 ** h & (table数组容量 - 1) **运算的到key在table数组中的索引位置!

2、为什么hashMap集合的table容量值建议设置为2的整数次幂次方 ?

首先 hashmap的数组容量大小都是2的次方大小时,hashmap的效率最高 。

因为当向map中插入或获取一个元素的时候,key在table数组中的索引位置,使用的是 h & (table数组容量 - 1) 算法来计算的,h可以是根据key计算出来的任何值,既然h可能是任意值,那么只有在数组容量上做文章,为了使数据尽可能均匀的分布在table数组上,那么数组的容量大小则至关重要。下面引用一个例子来说明

看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值