HashMap底层原理(超详细)

HashMap底层原理

1.底层实现

  • HashMap底层是哈希表,又称散列表
  • 当向hashmap使用put方法添加对象时,会进行去重(hash)

image-20250605103948754

  • 数据结构之哈希表又称散列表.
  • 是一种查询,插入等都较快的一种结构
  • 可以认为哈希表就是一个数组
  • 但是在Java的设计中,HashMap的哈希表是数组+链表+红黑树实现的

2.去重

通过实例解释

  • 创建集合,设置泛型Integer,加入数字,数字重复去重---->结果(去重)
  • 创建集合,泛型为学生对象(age,name),属性一致去重学生—>结果(没有去重)

实际操作中学生并没有去重,因为在集合中存取学生对象时,通过equals来判断存取的元素是否相等

image-20250605104609678

需要重写equals,不然会调用父类的equals(判断地址值,每个对象地址值都不一样,因此无法去重)

但重写equals 还是没有去重 ,为什么?

因为调用equals之前,先调用了hashCode()方法,获得地址值,在通过地址值向哈希表存储元素.具体是,将hash值当哈希表的下标存储,即每个对象的hash值都不相同,也就是存储到哈希表的位置不一样,所以认为元素不一样,直接存储,没有去重

因此,在重写equals判断元素内容相同的时候,也要重写hashCode ()方法让其地址值相同(但是性能不太好,下面有优化),这样的话,就会将hash值当哈希表的下标存储,他们就会存储到哈希表同一位置。

具体如下:

  • 先算出存储元素的地址值
  • 如果地址值不一样,直接根据hash值当下标存储到表中
  • 如果地址值一样,根据hash值当下标存储
    • 如果当前位置为空,直接存储
    • 如果当前位置有数据,调用equals比较内容是否相同
      • 如果内容相同,不存
      • 如果内容不相同,在同一位置形成链表,存储如下
      • image-20250605105957363

优化:(优化哈希冲突)

1.重写hashCode让地址值完全一致,会造成一个问题,就是每存储一个元素都要比较equals.且hash值当下标的,所有元素hash值一样,就会一直往一个地方放,形成超长链表,性能不好

image-20250605110155119

2.底层代码解决哈希冲突

image-20250605110405913

总结:无脑重写hashCode和equals

3.为什么是无序的

因为存储的数据,不是按顺序存的,是按hash值当下标存储的,每个对象的hash值不同,下标也就不固定,所以是无序的

4.扩容机制

  • 创建HashSet时,其实创建HashMap时底层hash表容量是16

  • 加载因子是0.75

  • 当存储的元素个数 >加载因子与当前容量的乘积时(0.75*16)=12时扩容(rehash),重构(创建大数组,原来数据打乱重排),变成原来的2倍

5.顺序为什么还会改变

因为,当存储元素超出阈值,即超出加载因子与当前容量的乘积时(0.75*16)=12时扩容,变成2倍,原来打乱重排,所以位置会变!!!

6 .底层代码

image-20250605111024507 image-20250605111050074 image-20250605111110524
  1. Java中HashMap的实现的基础数据结构是哈希表,其实就是数组,每一对key->value的键值对组成Entry类以双向链表的形式存放到这个数组中
  2. 元素在数组中的位置由key.hashCode()的值决定,如果两个key的哈希值相等,即发生了哈希碰撞,则这两个key对应的Entry将以链表的形式存放在数组中
  3. 调用HashMap.get()的时候会首先计算key的值,继而在数组中找到key对应的位置,然后遍历该位置上的链表找相应的值。

当然这张图中没有体现出来的有两点:

  1. 为了提升整个HashMap的读取效率,当HashMap中存储的元素大小等于桶数组大小乘以负载因子的时候整个HashMap就要扩容,以减小哈希碰撞
  2. 在Java 8中如果桶数组的同一个位置上的链表数量超过一个定值(8个),则整个链表有一定概率会转为一棵红黑树,在哈希表扩容时,如果发现链表长度 <= 6,则会由树重新退化为链表
  3. 链表转变成树之前,还会有一次判断,只有数组长度大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
  4. 当数组某一个桶内的链表节点数大于8,且数组容量小于64添加节点时,当链表节点数大于8时,触发treeifyBin方法,方法内判断数组小于64则进行扩容而不转为红黑树

整体来看,整个HashMap中最重要的点有四个:初始化数据寻址-hash方法数据存储-put方法,扩容-resize方法,只要理解了这四个点的原理和调用时机,也就理解了整个HashMap的设计。

put的过程源码

put的流程:
a、将传递的key通过扰乱算法计算一个新的值(目的:让hashcodel的高16位参与亦或运算使得hash值算的更加散列,减少hash碰撞)
b、如果是第一次添加元素,那么会初始一个长度为16的Node数组
c、如果不是第一次添加,那么会将hash值和数组的长度-1(算出来的值一定在0~数组长度-1之间)做与运算计算的出一个数组的下标
  通过计算的下标获取数组中对应位置上的值(有三种情况) 
  1、如果当前位置上的元素与要添加的元素相同,那么就覆盖原来的值 
  2、如果当前位置上的元素是一个红黑树节点,那么就将元素添加到红黑树上
  3、如果当前位置上的元素是一个链表,那么遍历当前链表 
  	1)如果要添加的元素与链表上的元素相同,则覆盖这个元素 
  	2)如果与链表上的元素都不相同,则添加到数组的尾部(尾插法) 
  	3)当链表的元素达到条件的时候,将链表转成红黑树 
  	  条件1:链表上元素个数大于8
  	  条件2:数的长度大于64 
 d、添加完元素后,如果元素个数大于阈值(数组的长度*0.75)则需要对数组进行扩容 
 
put方法的返回值 
 a、如果put进去的key不存在,那么返回null 
 b、如果put进去的key存在,那么返回被覆盖的原来的valuet值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab表示存放Node节点的数据   p表示当前节点   n表示长度  i表示节点在数组中的下标               
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组如果为空或者数组长度为0,那么就对数组进行扩容,数组默认初始大小为16
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //将数组的长度-1与hash值进行与运算(计算的结果一定是0~数组长度-1)得到元素应该存放的下标
    //如果当前下标位置为空,那么直接将Node节点存放在当前位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //如果当前位置不为空(分为三种情况)
    else {
        Node<K,V> e; K k;
        //情况1:要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接替换
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //情况2:如果要添加的元素是红黑树节点,那么将其添加到红黑树上
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //情况3:如果要添加的元素是链表,则需要遍历
        else {
            for (int binCount = 0; ; ++binCount) {
                //将当前元素的下一个节点赋给e
                //如果e为空,则创建新的元素节点放在当前位置的下一个元素上,并退出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果链表的元素个数大于8个(且当数组中的元素个数大于64),则将其转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接退出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果返回的e不为null
        if (e != null) { // existing mapping for key
            //将e的值赋给oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            //返回以前的值(当添加的元素已经存在返回的是以前的值)
            return oldValue;
        }
    }
    ++modCount;
    //如果数组的元素个数大于阈值则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize过程原码

HashMap的扩容时机
	如果是第一次扩容
    1)将定义一个长度为16的数组 
    2)阈值为数组的长度16*0.75
    
    当元素的个数大于阈值的时候,数组需要进行扩容 
    1)将数组的长度扩大一倍 
    2)将阈值也扩大一倍
为什么加载因子为0.75?
    加载因子影响到阈值
    如果加载因子太大,那么空间利用率高,但是查询效率降低
    如果加载因子太小,那么空间利用率低,但是查询效率提高
为什么数组的长度是2的次方数?
   减少计算出来的下标的重复的几率(减少hash碰撞)
final Node<K,V>[] resize() {
    //oldTab 表示原来数组(如果是第二次扩容:长度为16的那个)
    Node<K,V>[] oldTab = table;
    //oldCap 表示原数组的容量(长度)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr 表示数组原来的阈值 12
    int oldThr = threshold;
    //newCap 新数组的容量 newThr 新数组的阈值
    int newCap, newThr = 0;

    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新数组的容量扩大一半  newCap 32
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //新阈值扩大老阈值的一半  newThr 24
            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 = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //threshold 24
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建一个长度为32的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //table指向新数组
    table = newTab;
    if (oldTab != null) {
        //将原数组中的元素拷贝到新数组中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果当前位置元素不为空
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //情况1:当前位置上的下一个元素为空,则直接将这个元素拷贝到新数组中
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;    
                //情况2:当前位置上的元素红黑树类型,则需要进行切割
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //情况3:当前位置上的元素链表类型,则需要进行分散拷贝
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get的过程原码

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //当前first与要找到的hash和key都相等直接返回当前这个first元素
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果当前first不为空(有两种情况)
        if ((e = first.next) != null) {
            //当前位置是一个红黑树
            if (first instanceof TreeNode)
                //根据hash、key从红黑树上找到对应的元素
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //当前位置是一个链表
            do {
                //循环进行比较直到找到向的hash和key的元素,并返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //如果数组的为空、数组的长度为0、当前下标位置上的值为null,这三种情况都返回null
    return null;
}

`

环进行比较直到找到向的hash和key的元素,并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果数组的为空、数组的长度为0、当前下标位置上的值为null,这三种情况都返回null
return null;
}


`

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值