HashMap底层与原理详解

为什么面试老是问HashMap

在这里插入图片描述

追根溯源

之前,我的学长就告诉我说,好好看HashMap,去理解它,去弄明白他的实现,你将受益匪浅,年少的我不懂事啊,会用就行了呗,直到面试给我上了生动的一课,要是能重来,我要选韩信,先打红buff…… 在这里插入图片描述 作为应届生的我们,公司知道我们的项目经验不足,所以公司更看重我们的基础知识,而HsahMap的底层既有数组,链表,红黑树等很多的基础知识,所以HashMap成了重灾区,不仅能考察基础知识的掌握程度,还能考察基础知识掌握的深度。

情景再现

面试官:先来个自我介绍吧。 我:我来自王者峡谷学院,擅长java…… 面试官:好的,了解了,先考察一些基础吧,了解HashMap嘛,说说他的底层实现吧 我:好的,HashMap底层是数组+链表+红黑树嘛 面试官:你可以再补充补充 我:HashMap以键值对的方式存储数据 面试官:好的,了解了,你有什么问我的嘛,我知道你没有,那就到这里,回去等通知吧 我:我这是稳了呀! 三分钟后,感谢您的投递,您已进入我公司人才储备库,有缘再见(再也不见)。 我:漂亮。 在这里插入图片描述

HashMap的底层实现原理

直接上图 在这里插入图片描述 是不是啥也没看懂,那就听我详细给你说说吧。

定义的常量及其使用

上底层代码

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

 private static final long serialVersionUID = 362498820763181265L;
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始大小
  static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
  static final float DEFAULT_LOAD_FACTOR = 0.75f;//装载因子
  static final int TREEIFY_THRESHOLD = 8;//链表最大长度
  static final int UNTREEIFY_THRESHOLD = 6;//红黑树最小长度
  static final int MIN_TREEIFY_CAPACITY = 64;//最小键值对数量
复制代码
public abstract class AbstractMap<K,V> implements Map<K,V> {
复制代码
这咋这么一大堆,听我慢慢道来
首先可以看到 HashMap是继承AbstractMap,实现了Map这个接口,
也就是说HashMap是基于Map接口实现,可以clone和序列化,
不懂的等我以后更新文章了再去看吧。(我不是懒,真不是)
复制代码

然后便是这几个常量(又臭又长),其实看看英文就知道是啥意思了

DEFAULT_INITIAL_CAPACITY 是默认初始大小 16
MAXIMUM_CAPACITY   是最大容量  1073741824
DEFAULT_LOAD_FACTOR  是默认装载因子(敲黑板)比较重要
TREEIFY_THRESHOLD 是链表的最大长度  当链表长度超过8就会转换成红黑树
UNTREEIFY_THRESHOLD 是红黑树最小长度 当树长度小于6就会转换为链表
MIN_TREEIFY_CAPACITY  是最小键值对数量  当键值对超过64时才会转换
这是为了避免在哈希表建立初期,
多个键值对恰好被放入了同一个链表中而导致不必要的转化。
复制代码

我也不知道它多大,于是我就去求了一下(大家看个热闹就行) 在这里插入图片描述
这些常量有啥用啊,我们接着往下看

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
复制代码

我们一般用HashMap的时候都是new Hash() 不会去传参,当不传参数时,转载因子就会初始化成0.75 。

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
复制代码

我们也可以传参,可以传容量和装载因子,但是如果传的容量过大,就会初始化成默认的最大容量。

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//对象的hash值
        final K key;//键
        V value;//值
        Node<K,V> next;//指向下一个元素
复制代码

底层定义了实现Map.Entry接口的Node节点,正是这一个个Node节点连接组成的链表。

put()方法详解

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
复制代码

可真是博(you)大(chou)精(you)深(chang)呢 一上来就能看见put()方法调用了putValue这个方法,然后传参时调用了hash()这个方法

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
复制代码

有人会问我,为什么不直接调用key本身的hsah值呢,还要再写一个方法呢,其实这个hash函数很有说法的

哈希函数 ,也叫散列值优化函数,也叫扰动函数
用原来的哈希值,进行扰动运算, 来生成一个新的哈希值,降底哈希冲突的概率
复制代码

然后便是真正的put过程了,首先判断数组是不是空的,如果是空的就会初始化数组,然后判断当前节点是否为空,空的话就新建一个Node,把value值存放进去,如果不为空,就比较hash值,如果hash值相同,则替换新的value,返回旧的value,这里还会有一个判断,会判断有没有树,如果有树了,就会按照树的结构去处理,不相同则尾插法放在这个节点的后面。

怕文字太长不乐意看,看图吧!! 在这里插入图片描述

扩容机制

 ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
复制代码

最后这个位置会有容量判断,如果容量到达了阈值就会进行扩容,这个阈值是容量(现有容量,不是最大容量)乘以转载因子,也就是说如果装载因子越小,扩容触发的就会越频繁,扩容的后的容量为原来的二倍,原node会再次进行hash函数(扰动计算)产生新的hash,再次排序。 px:扩容这里的源代码太长了,细心的小伙伴们可以自己去看源代码(我赌不超过五个会去)

小经验分享

不要死记硬背,要去理解原理,不然有些问题你还是会打不上来 比方说 面试官:说的这么详细,那为什么扩容是两倍啊,我为啥不能扩容三倍啊 我:他就那么写的,可能底层开发者觉得两倍好呗。 在这里插入图片描述 其实要是细心的看过源码就会明白

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//就是这里
            tab[i] = newNode(hash, key, value, null);
复制代码

在put方法的这一行,i = (n - 1) & hash 计算位置时用容量-1与hash进行&运算,容量为2倍的方式扩容,所以他的容量总是2的幂次方,这样-1之后,二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。

快给底层开发者道歉,大佬们的智慧你这凡人怎可理解 在这里插入图片描述

你觉得这不能说明什么,那就再来一个, 面试官:链表长度超过8的时候转换成红黑树,红黑树容量小于6的时候会转换成链表,为什么我不能设置成7呢,干嘛一个8,一个6的。 我:66大顺,88大发嘛,图个吉利 在这里插入图片描述 原因:   红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

不用7原因是:   中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

最后给你们推荐一本书吧

在这里插入图片描述

这里好多都是本人自己的理解,如果有错误请拿来打我的脸 在这里插入图片描述 下课!

猜你喜欢

转载自juejin.im/post/7018392590542176270