面试中为什么会经常问HashMap?

157 阅读2分钟

在面试中,面试官为什么会经常问HashMap呢? 首先,HashMap在编程过程中使用的频率非常高,所以会考察对它的掌握程度。 其次,HasHMap涉及的知识面非常广泛,它涉及了数组,链表,红黑树,哈希算法等知识点,通过它可以很好的考察被面者的基础知识。 HashMap是用来存储Key-Value键值对的一种集合,这个键值对也叫Entry。 JDK1.7中,HashMap是通过数组+链表来实现的,而到了JDK1.8则是数组+链表+红黑树实现,下边内容主要是针对JDK1.8版本的HashMap。
先看一下HashMap的数据结构图:

截屏2022-09-26 09.10.50.png HashMap的几个关键属性:

    /**
     * 默认初始容量 - 必须是 2 的幂。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     *最大容量
     */
    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;

HashMap根据key的hash值来存储数据,HashMap最多只允许一个key为null的记录。

Hash算法: 就是根据设定的Hash函数H(key)和处理冲突方法,将一组关键字映射到一个有限的地址区间上的算法。

Hash冲突: 由于用于计算的数据key是无限的H(key),而映射到的区间是有限的,所以肯定会存在两个key使得H(key1)=H(key2),这就是hash冲突。HashMap采用链地址法(拉链法)解决冲突问题。即将哈希值相同的元素构成一个链表,并将单链表的头指针存放在哈希表的第i个单元中(通过H(key)计算得到的index所对应的数组位置),查找、插入和删除主要在链表中进行。链表法适用于经常进行插入和删除的情况。

HashMap中put元素的过程: 假如要在table[I]位置插入: 1 首先判断table[I]是否为空,如果为空直接插入 2 如果不为空,判断当前元素的key和待插入元素的key是否相同,相同则替换 3 如果key不同,判断当前位置是否为treeNode,即table[I]是不是红黑树,如果是的话直接在 树中插入该元素 4 如果当前位置是链表,判断是否长度大于8,并且达到了转换成红黑树的容量阀值,如果满足条则将链表转换为红黑树,在红黑树中插入,否则遍历链表,如果遍历到相同的key,则替换,否则链表下一个节点指向该元素。

 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;
        //计算落在 hash数组的位置,如果当前位置为空,直接在当前位置添加节点    
        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))))
                 //如果key相同,则替换当前元素
                e = p;
            else if (p instanceof TreeNode)
				//如果当前是树结构,向树上添加元素            
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //当前结构依然是链表,遍历链表,直到末尾或者找到 key相同的元素替换
                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) { //如果是已经存在的元素,判断是否替换
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果超过容量阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap中的扩容机制: 当HashMap中元素的个数达到了阈值,(capacity * load factor), DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR = 0.75f,元素个数超过16* 0.75 = 12时,将数组的大小扩展为2* 16 = 32,然后重新计算每个元素在hashmap中的位置数组 HashMap中,元素存放的位置与hash数组的个数是有关系的(tab[i = (n - 1) & hash]),所以当发生扩容时,hash数组的个数发生了变化,这个时候,元素也需要重新进行hash计算。由于put时计算hash数组角标是通过i = (n - 1) & hash计算的,其中n是的2的x次幂,扩容时,容量变为原来的两倍,n-1 == (n-1)<<1 & 1,所以hash数组中的元素,要么还在原来的index位置,要么在原index往后移动原容量大小的位置。

当HashMap中一个链表的节点个数达到8(TREEIFY_THRESHOLD = 8)个时,如果数组长度没有达到64,那么HashMap会扩容。如果达到64,就会首先将该链表转换成红黑树,Node类型从Node变成TreeNode。相反,移除元素,下一次执行reset()方法时,会判断树中的节点数,如果小于6(UNTREEIFY_THRESHOLD = 6),也会将树转换为链表。