HashMap源码分析及高频面试问题答案

本文详细剖析了HashMap的内部结构,包括其继承体系、成员变量、构造方法、扩容机制、hash算法及putVal方法流程。探讨了链表与红黑树转换条件,以及扩容规则和效率提升策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先看下继承结构

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

继承自AbstractMap,并实现了Map等方法。

主要成员变量和一些常量

	//默认初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //由链表转为红黑树的阈值=8
    static final int TREEIFY_THRESHOLD = 8;
    //由红黑树转化为链表的阈值=6
    static final int UNTREEIFY_THRESHOLD = 6;
    //最小的红黑树容量阈值=64,即容量大于等于64时才才会从链表变为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;
    //节点数组
    transient Node<K,V>[] table;
    //实际元素个数
    transient int size;
    //负载因子
    final float loadFactor;
    //扩容阈值
    int threshold;

构造方法

1.无参构造方法

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

如果不传负载因子,则将负载因子设为默认的,即0.75。

2.传入容量及负载因子

public HashMap(int initialCapacity) {
  this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}

根据传入的参数,生成相应的容量及负载因子。

tableSizeFor方法

static final int tableSizeFor(int cap) {
   int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法比较巧妙,通过位运算,得到一个稍微大于或等于传入参数的2的n次方的数值,作为容量值返回。
例如如果你传入7,则自动会使容量为8。即稍大的2的N次方数据。

需要注意:
以上三种构造方法,生成map时,Node<K,V>[] table仍然为null。即数组并未在此时初始化,而是再之后添加真实数据时才真正生成该数组。

3.传入一个map作为参数的构造方法

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

传入一个map,会对map遍历添加到当前map,涉及到扩容resize,和添加元素putVal,hash几个操作。

先看hash操作

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

此操作将元素key的hashcode进行扰动,即高16位与低16进行异或运算。使key能够更为平均地分配到table上。

添加元素putVal

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为空则先扩容
    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);
                    如果链表node数量等于树化阈值,进行树化操作
                    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;
            }
        }
        //e !=null 说明map中已存在相同元素
        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;
}
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }

上面的代码获取数组下标的操作要着重说下:(n - 1) & hash 这个操作很巧妙,由于n是一个2的某次方,此时(n - 1) & hash 就等同于 hash % n 即取模运算。如10 模 8 就等于 10(1010) 与 7(0111) 都等于2 。显然位运算比模运算要高效的多。
预留的钩子操作,我们将会在后续文章中看到其用法。

扩容resize()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果数组长度大于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
    }
    //如果数组为null,阈值>0,说明已经通过前三种构造函数生成map,将阈值设置为旧有阈值即可
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //如果数组为null,阈值为0,则将阈值和容量设置为默认
    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 = newThr;
    //创建新的数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //旧数组不存在
    if (oldTab != null) {
    //遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //数组中的元素不为null
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //该元素的下一个元素为null,说明链表长度为1,即只有头节点
                if (e.next == null)
                //将该数据放到新数组中对应位置
                    newTab[e.hash & (newCap - 1)] = e;
                //如果存在树结构的下一个节点,进行拆分树
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //链表结构存在多个节点,将链表易一拆为二,分到新数组中
                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;
                    }
                    //将高位链表放入旧数组长度+j,即数组对应的高位
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

总的来说就是,将数组扩容一倍,阈值扩大一倍。并将旧数组或链表拆分到新的链表上。即实现扩容。
拆分规则如下(e.hash & oldCap) == 0则方法原先位置,否则放在oldCap+原位置。
这个操作也是利用了容量为2的N次幂的特性,用与运算代替取模运算。
上文看到旧有的位置计算使用hash与数组长度-1的与运算,代替hash对数组长度的取模运算。即位置=e.hash&(oldCap-1)=e.hash%oldCap。
那么扩容之后的位置=e.hash&(oldCap<<1-1),由于oldCap是2的某次幂,所以oldCap<<1-1就比oldCap-1多一位1。即如果oldCap=8,那么二进制oldCap<<1-1=1111,而oldCap-1=111。从而e.hash & oldCap即e.hash与oldCap与运算为0,也就是e.hash与oldCap<<1-1的最高位即1000与运算为0,结果自然等同于e.hash&(oldCap-1)。所以位置不变,放入低位。
那么如果e.hash & oldCap即e.hash与oldCap与运算为1,说明e.hash&(oldCap<<1-1)结果等于e.hash&(oldCap-1)加上一个最高位代表的数字,即oldCap,即原位置+旧数组长度。
从而实现既定的规则。

面试中经常问到的相关问题:

1.说一下HashMap的数据结构
Hashmap是用数组加链表或红黑树实现的,当链表长度大于8且数组长度大于等于64时,链表转化为红黑树。当红黑树的节点数量小于6时,转化为链表。

2.说一下put方法的完整流程
1.校验数组是否存在且长度大于0,否则扩容数组。
2.根据key的hashcode高低位扰动后的hash值,与数组长度-1做与运算,代替hash对数组长度的取模运算,得到放入数组的位置。
3.如果该位置不存在数据,将数据放入该点。
4.如果该位置存在数据,判断数据类型,如果是红黑树则将数据插入红黑树,否则插入链表尾部。如果该数据已存在,则不进行操作,直接返回该值。
5.判断容量是否超出阈值,如果超出进行扩容。

3.说一说hashmap的扩容机制
1.一般将数组扩容为之前的一倍。
2.将数组上的红黑树节点或链表节点,一分为二,放入新数组的高位链表和低位链表。
3.将两个链表根据计算规则,放入新数组的对应位置。计算方式是将hash值与旧数组长度进行与运算,得0放入低位,即原先位置。为1放入高位,即旧数组长度+原先位置。

4.为什么要扩容为2倍?
要遵守数组长度为2的幂数规则,该规则使用位与运算代替取模运算,更为高效地找到key所在的位置。

5.HashMap中的链表替换为数组可以吗?时间复杂度相同吗?
可以。时间复杂度不同。数组在增删时需要进行数据copy,时间负载度为O(n-i),n是数组长度,i是数据位置。而链表增删时间复杂度为O(1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值