HashMap核心原理和扩容机制

182 阅读4分钟

1.HashMap背景和核心技术点:

1.HashMap的作用是什么?

  • HashMap就是一个内存数据库,数据库用来存取数据的。数据结构 K,V
  • HashMap的核心功能是通过键值对的方式存储和检索数据,通过使用哈希函数将键映射到存储桶中,从而实现高效的数据访问和搜索。

2.使用HashMap

  • HashMap map = new HashMap();
  • map.put();
  • map.get();

3.HashMap这个数据库,数据是存在内存中

Redis、Mysql(bufferpool,通过异步线程进行刷盘)、zk、ectd、nacos也好,他们的数据都是先存在内存中的,然后通过各种多线程或者异步进行持久化。

4.在计算机当中,内存存数据有哪些结构呢?

  • 数组
  • 链表
  • hash表

5.HashMap底层到底是用什么来存数据的?

hash表

6.hash表是什么?

底层就是一个数组

7.既然已经有了数组,为什么又要提出一个hash表的概念呢?hash表跟数组有什么区别呢?

//数组的使用 
String[] arr = new String[16]; 
arr[0] = "test1"; 
arr[1] = "test2"; 
//通过数组索引下标取值 
System.out.println(arr[0]); 
//创建HashMap通过put和get方法进行数据的读写,未指定下标 
HashMap map = new HashMap(); 
map.put("name","test"); 
map.get("name");`

普通数组在读写数据的时候,需要人为给定下标,这样才能实现读写操作。

而hash表,不需要人为给下标。就可以实现读写操作。

8.Hash表中的下标到底是怎么得来的呢?

Hash值是int类型的

int类型是4Byte字节,1Byte等于8位二进制

int类型是32位数字

hash值 i = hash &(长度-1)

如果数组长度16,那i最大是15,就是4位2进制的最大值

9.通过上面算法去求下标,会不会存在什么问题呢?

hash冲突

10.一旦hash冲突了,怎么解决呢?

  • 一个睡床上,一个睡床下;链式地址法,也是HashMap的默认解决方案。node.next();
  • 换房间;线性探测法,是ThreadLocal底层的默认解决方案。

for(int i = hash&(c-1);arr[i]!=null;i++)

  • 重新计算Hash值

11.有没有什么方法能减少hash冲突发生的概率?

  • (h = key.hashCode()) ^ (h >>> 16) 专业术语叫扰动函数能在一定程度减少hash冲突的概率。
  • 为什么数组的长度是2的幂次方

12.为什么在new HashMap的时候没有创建数组?

这种思想叫懒加载思想,只有在你真正用的时候,才会创建数组,你实例化的时候,并不会创建

这种思想会从技术底层帮我们节约资源,数组是存在堆内存中,如果实例化就创建数组,就会频繁占用堆内存导致资源浪费。

13.HashMap中的key可不可以为null呢?如果可以,那么null的key可以有多少个?

1.可以为null,只会存在一个为null的key,其余为null的key会被覆盖

14.HashMap底层的数组默认长度是多少?

底层默认长度4>> = 16

15.HashMap的扩容因子是多少?

底层默认是0.75

16.链表转换红黑树条件有两个,而且必须同时满足

  • 链表的长度条件必须大于等于8
  • 数组的长度必须大于等于64

在使用过程中会出现什么问题?

并发安全问题,加锁

2.原理和源码:

一定要先根据核心功能推到流程,在根据流程去看源码验证。

HashMap的核心功能是存数据的-->首先得有HashMap对象,才能调用它的方法去存

1、new HashMap的流程

public HashMap() { 
    this.loadFactor = 0.75; // all other fields defaulted 
} 
//在new对象的时候,HashMap的数组是null,并没有创建
//这种思想叫懒加载思想,只有在你真正用的时候,才会创建数组,你实例化的时候,并不会创建

2、map.put的流程

public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
  • 计算K的Hash值
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
  • 如果key为null,那么三目运算得到的hash值是0 00000000 000000000 00000000 00000000
    (h = key.hashCode()) ^ (h >>> 16) 专业术语叫扰动函数

  • 会给我们创建一个数组

必须先拿到一个初始数组长度

final Node<K,V>[] resize() { 
    Node<K,V>[] oldTab = 0; 
    int oldCap = 0; 
    int oldThr = threshold; 
    int newCap, 
    newThr = 0; 
    if (oldCap > 0) { 
        if (oldCap >= MAXIMUM_CAPACITY) { 
            threshold = Integer.MAX_VALUE; 
            return oldTab; 
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
            oldCap >= DEFAULT_INITIAL_CAPACITY) 
            newThr = oldThr << 1; // double threshold 
        } else if (oldThr > 0) // initial capacity was placed in threshold 
            newCap = oldThr; 
        else { // zero initial threshold signifies using defaults 
            newCap = 16;
            newThr = (int)(0.75 * 16); 
        } 
        if (newThr == 0) { 
            float ft = (float)newCap * loadFactor; 
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 
            (int)ft : Integer.MAX_VALUE); 
        } 
        threshold = newThr;// 12 
        @SuppressWarnings({"rawtypes","unchecked"}) 
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
            table = newTab; 
            return newTab; 
    }
  • 再通过i=hash&(长度-1)获得下标
  • 然后通过下标去存数据tab[i] = new Node("test",38);
  • 在存的过程中会出现冲突

一旦发生hash冲突

    • 先判断跟我冲突的是不是我自己,如果hash值和key都是我自己,那么进行等值覆盖。
    • 如果不是我自己,则判断数组当前的节点是不是红黑树类型,如果是,则执行树的插入
    • 如果不是红黑树,那么就进行插入链表