HashMap基本用法

Map<String, Object> map = new HashMap<>();
map.put("student", "333");//正常入数组,i=5
map.put("goods", "222");//正常入数据,i=9
map.put("product", "222");//正常入数据,i=2
map.put("hello", "222");//正常入数据,i=11
map.put("what", "222");//正常入数据,i=3
map.put("fuck", "222");//正常入数据,i=7
map.put("a", "222");//正常入数据,i=1
map.put("b", "222");//哈希冲突,i=2,product.next
map.put("c", "222");//哈希冲突,i=3,what.next
map.put("d", "222");//正常入数据,i=4
map.put("e", "222");//哈希冲突,i=5,student.next
map.put("f", "222");//正常入数据,i=6
map.put("g", "222");//哈希冲突,i=7,fuck.next

首先我们都是创建一个Map对象,然后用HashMap来实现,通过调用?put?get?方法就可以实现数据存储,我们就先从构造方法开始分析

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

初始化负载因子为0.75,负载因子的作用是计算一个扩容阀值,当容器内数量达到阀值时,HashMap会进行一次resize,把容器大小扩大一倍,同时也会重新计算扩容阀值。扩容阀值=容器数量 * 负载因子,具体为啥是0.75别问我,自己查资料吧(其实我是不知道,我觉得这个不重要吧~)

继续看?put?方法

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

额,也没啥可看的,继续往下看putVal方法吧

transient Node<K,V>[] table;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //先判断当前容器内的哈希表是否是空的,如果table都是空的就会触发resize()扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //通过 (n - 1) & hash 计算索引,稍后单独展开计算过程
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果算出来的索引上是空的数据,直接创建Node对象存储在tab下
        tab[i] = newNode(hash, key, value, null);
    else {
        //如果tab[i]不为空,说明之前已经存有值了
        Node<K,V> e; K k;
        //如果key相同,则需要先把旧的 Node 对象取出来存储在e上,下边会对e做替换value的操作
        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 {
            //在这里解决hash冲突,判断当前 node[index].next 是否是空的,如果为空,就直接
            //创建新Node在next上,比如我贴的图上,a -> aa -> a1
            //大概逻辑就是a占了0索引,然后aa通过 (n - 1) & hash 得到的还是0索引
            //就会判断a的next节点,如果a的next节点不为空,就继续循环next节点。直到为空为止
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果当前这个链表上数量超过8个,会直接转化为红黑树,因为红黑树查找效率
                    //要比普通的单向链表速度快,性能好
                    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;
            }
        }
        //只有替换value的时候,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;
}

虽然写我加了注释,但是我还是简单说一下这个的逻辑吧
1.首先判断哈希表,是否存在,不存在的时候,通过resize进行创建
2.然后在通过索引算法计算哈希表上是否存在该数据,不存在就新增node节点存储,然后方法结束
3.如果目标索引上存在数据,则需要用equals方法判断key的内容,要是判断命中,就是替换value,方法结束
4.要是key也不一样,索引一样,那么就是哈希冲突,HashMap解决哈希冲突的策略就是遍历链表,找到最后一个空节点,存储值,就像我的图一样。灵魂画手有木有,很生动的表式了HashMap的数据结构
5.最后一步就是判断是否到扩容阀值,容量达到阀值后,进行一次扩容,按照2倍的规则进行扩容,因为要遵循哈希表的长度必须是2次幂的概念

好,put?告一断落,我们继续?get?吧

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

get方法,恩,好,很简单。hash一下key,然后通过getNode来获取节点,然后返回value,恩。get就讲完了,哈哈。开个玩笑。我们继续看getNode

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //哈希表存在的情况下,根据hash获取链表的头,也就是first对象
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //检测第一个first是的hash和key的内容是否匹配,匹配就直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //链表的头部如果不是那就开始遍历整个链表,如果是红黑树节点,就用红黑树的方式遍历
            //整个链表的遍历就是通过比对hash和equals来实现
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

我们在整理一下,get方法比put要简单很多,核心逻辑就是取出来索引上的节点,然后挨个匹配hash和equals,直到找出节点。
那么get方法就搞定了

再来看一下resize吧。就是HashMap的扩容机制


final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //检测旧容器,如果旧容器是空的,就代表不需要处理旧数据
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //保存扩容阀值
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 对阀值进行扩容更新,左移1位代表一次2次幂
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold

### 最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

### 美团面试经验
![美团面试](https://s2.51cto.com/images/20210829/1630172077964035.jpg)
字节面试经验
![字节面试](https://s2.51cto.com/images/20210829/1630172077202114.jpg)
菜鸟面试经验
![菜鸟面试](https://s2.51cto.com/images/20210829/1630172077291734.jpg)
蚂蚁金服面试经验
![蚂蚁金服](https://s2.51cto.com/images/20210829/1630172078198334.jpg)
唯品会面试经验
![唯品会](https://s2.51cto.com/images/20210829/1630172078250066.jpg)

>因篇幅有限,图文无法详细发出,感兴趣的朋友[可以点击这里前往我的腾讯文档](https://gitee.com/vip204888/java-p7)免费获取上述资料!