0
点赞
收藏
分享

微信扫一扫

一篇:HashMap

Jonescy 2022-03-23 阅读 162
java架构

https://cdn.pixabay.com/photo/2022/03/01/20/58/peace-genius-7042013_960_720.jpg

HashMap

HashMap的底层数据结构是啥?

好好的数组+链表为啥要引入红黑树?

你再理解下哈希表的好处吧

比如有一串数据[a,e,r,h,s,y,p,d]如果要查找a,只能一个一个的比较,最多会比较8次;

如果使用hash表的话,在put时,先会a计算出a的二次hash=97,然后mod16=1,然后将a存进数组下标为1处。

查找的时候以同样的方法计算出a的索引,就可以快速定位到你想要的数据了,最好的时间复杂度为O(n)。

HashMap链表过长怎么办?

不就是链表长吗,我要么给你把链表长度减短一点或者链表转成红黑树

在数组扩容时,元素的下标会重新计算。But,扩容也不一定缩小链表长度,如图:

image-20220215233230216

别慌,这个时候就可以树化了。树化有两个条件:【链表长度大于8 && 数组长度大于等于64】

树化后一般都会减少查找次数,因为红黑树嘛,检查次数最多不超过树的高度。

HashMap中链表的长度有可能超过8。

树化的两个条件是啥?

为啥不一上来就树化?

  • 没必要,元素很少的情况下,链表查找代价<(树化+树查找)代价

  • 而且树节点的内存占用>链表

树化阈值为什么是8呢?

hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率时0.000000006

树的退化来啦 哈哈哈哈

HashMap什么时候扩容?

  • HashMap容量(capacity):默认 16
  • HashMap负载因子(loadFactor):默认 0.75。
    • 负载因子=size / capacity 这里的size是填入HashMap的元素的个数,而不是数组(桶)的填充数量。比如数组的长度是16 ,占用桶的数量可能只有10,但是因为链表的存在可以存入12个元素。当存入第13个元素时才会进行扩容,注意是 添加元素后
  • HashMap阈值(threshold):阈值=容量*加载因子。默认 12当元素数量>阈值时便会扩容。

HashMap扩容时都干了些啥?

为每个元素重新计算桶下标

讲讲HashMap的扩容机制

当xxxx(见上),就会触发扩容机制。**扩容到原来的2倍。**但是HashMap的容是上限的,为Integer.MAX_VALUE。如果超出了这个数,就不再增长,且阈值为Integer.MAX_VALUE

数组是无法扩容的,所以只能新建一个大数组,然后把数据copy过去。扩容方法resize()

    /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.【这有阻止未来调用的效果】
     * @param newCapacity the new capacity, MUST be a power of two;【一定是2的幂】	
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
	 void resize(int newCapacity) {	// newCapacity为新的容量
	     // 小数组,临时过度下
	     Entry[] oldTable = table;
	     // 扩容前的容量
	     int oldCapacity = oldTable.length;
	     // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30
	     if (oldCapacity == MAXIMUM_CAPACITY) {
	         // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1
	         threshold = Integer.MAX_VALUE;
	         return;
	     }
	 
	     // 初始化一个新的数组(大容量)
	     Entry[] newTable = new Entry[newCapacity];
	     // 把小数组的元素转移到大数组中
         // void transfer(Entry[] newTable, boolean rehash)
	     transfer(newTable, initHashSeedAsNeeded(newCapacity));
	     // 引用新的大数组
	     table = newTable;
	     // 重新计算阈值
	     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
	 }

transfer用来将小数组的数据copy到新数组中去。


    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        //新的容量
        int newCapacity = newTable.length;
        //遍历小数组
        for (Entry<K,V> e : table) {
            while(null != e) {
                //拉链法
                Entry<K,V> next = e.next;
                //是否要重新计算hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //根据大数组的容量和键的hash计算元素在数组的下标
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

HashMap索引的计算方法是啥?

注意:二次hash是为了配合容量是2的n次幂这一设计前提,如果hash表的容量不是2的n次幂,则不需要进行二次hash

HashMap容量为何是2的n次幂?

讲讲put方法的流程

1.7与1.8的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且该索引下没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化(旧的hash值跟当前的数组容量按位与,如果是0,说明不用动位置; 不是0,旧索引+旧容量)

扩容(加载)因子为何默认是 0.75f?

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

HashMap下的并发问题

HashMap并发下的数据错乱是怎么回事?

数据错乱(1.7,1.8 都会存在)

  • 代码参考 day01.map.HashMapMissData,具体调试步骤参考视频
public class HashMapMissData {
    public static void main(String[] args) throws InterruptedException {

        HashMap<String, Object> map = new HashMap<>();
        Thread t1 = new Thread(() -> {
            map.put("a", new Object()); // 97  => 1
        }, "b");

        Thread t2 = new Thread(() -> {
            map.put("1", new Object()); // 49 => 1
        }, "a");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(map);
    }
}

image-20220216123743326

1.7中HashMap的扩容死链是怎么回事?

扩容死链(1.7 会存在)

1.7 源码如下:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

前提说明
image-20220323085345063

在链表迁移时并不会创建新的对象,上图其实只有两个对象:a和b。链表迁移只是改变了引用而已。

死链产生的根源:迁移前顺序是a、b,迁移后顺序是b、a。

  • e 和 next 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

image-20210831084325075

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

image-20210831084723383

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

image-20210831084855348

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

image-20210831085329449

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

image-20210831085543224

HashMap中key的设计

如果 key 可变,例如修改了 age 会导致再次查询时查询不到

 @Test
    public void test02161254(){
        Person person = new Person("稚浩",21);
        HashMap<Object, Object> map = new HashMap<>();

        map.put(person,new Object());

        System.out.println(map.get(person));

        person.setAge(18);

        System.out.println(map.get(person));

    }
java.lang.Object@4dcbadb4
null

hashcode()的设计

String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 S i S_i Si,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S 0 ∗ 3 1 ( n − 1 ) + S 1 ∗ 3 1 ( n − 2 ) + … S i ∗ 3 1 ( n − 1 − i ) + … S ( n − 1 ) ∗ 3 1 0 S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0 S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 $32 ∗h -h $
    • 2 5 ∗ h − h 2^5 ∗h -h 25hh
    • h ≪ 5 − h h≪5 -h h5h

de() 设计**

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 S i S_i Si,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S 0 ∗ 3 1 ( n − 1 ) + S 1 ∗ 3 1 ( n − 2 ) + … S i ∗ 3 1 ( n − 1 − i ) + … S ( n − 1 ) ∗ 3 1 0 S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0 S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 $32 ∗h -h $
    • 2 5 ∗ h − h 2^5 ∗h -h 25hh
    • h ≪ 5 − h h≪5 -h h5h
举报

相关推荐

0 条评论