Java基础面试专栏(三十四):HashMap高频四问(扩容/容量/get/插入次数)

3 阅读11分钟

HashMap是Java开发中最常用的集合类之一,也是面试中的高频考点,尤其围绕“扩容”“容量”“get方法原理”“插入元素扩容次数”这四个问题,几乎是面试必问。很多开发者只知道表层用法,却不清楚底层逻辑,面试时常常答不全面、不精准。

本文将完全贴合面试答题逻辑,围绕这四个核心问题,按“核心结论+底层原理+实例验证+面试模板”的结构,逐一拆解,仅围绕核心内容展开,搭配可直接运行的代码示例,帮大家吃透HashMap的核心特性,轻松应对面试提问和实际编码。

面试万能开场白(直接套用,快速定调):面试官您好,关于HashMap的四个高频问题,核心结论可以先明确——第一,扩容为原来2倍时,数据要么留在原下标,要么搬到原下标+旧容量位置;第二,默认容量是16,默认加载因子0.75;第三,默认配置下插入100个key会扩容3次;第四,get方法核心是通过hash定位下标,再遍历链表/红黑树查找。

一、第一问:HashMap长度扩容两倍,里面的数据会怎么变化?

核心结论(面试先直接说):HashMap扩容为原来2倍后,数组长度变为原来的2倍,里面的每个节点要么留在原下标位置,要么搬到“原下标+旧容量”的位置,不会乱序,且原来的一条链表会拆成两条,分别放在两个位置

1. 底层原理:为什么是2倍?为什么数据只去两个位置?

HashMap定位数组下标的核心公式是:i = hash & (数组长度 - 1),这也是扩容必须是2倍的核心原因——数组长度始终是2的幂,此时“数组长度-1”的二进制是全1(比如长度16→15,二进制1111;长度32→31,二进制11111),能保证hash值与运算后下标分布均匀。

扩容为2倍后,数组长度增加1位二进制位,此时计算下标时,会多1位二进制参与运算,这一位的取值只能是0或1:

  • 若新增的二进制位是0:运算后下标与扩容前一致,数据留在原位置;

  • 若新增的二进制位是1:运算后下标 = 原下标 + 旧数组容量,数据搬到新位置。

2. 简单示例:直观理解数据变化

假设旧数组长度为16(二进制10000),旧容量-1=15(二进制1111);扩容后数组长度为32(二进制100000),新容量-1=31(二进制11111)。

现有一个key的hash值二进制最后5位为「11010」:

  • 扩容前:用最后4位「1010」与15(1111)做与运算,下标=10;

  • 扩容后:用最后5位「11010」与31(11111)做与运算,下标=26,而26=10+16(旧容量)。

若另一个key的hash值二进制最后5位为「01010」:

  • 扩容后下标仍为10,数据留在原位置。

3. 数据分配细节(JDK1.8)

JDK1.8对扩容做了优化,无需重新计算每个key的hash值,直接将原来的一条链表拆成两条:

  • 低位链表:新增二进制位为0的节点,留在原下标;

  • 高位链表:新增二进制位为1的节点,搬到「原下标+旧容量」的位置。

补充说明:红黑树扩容时,也会按同样规则拆分,若拆分后树的节点数过少(小于6),会退化为链表;且JDK1.8扩容不会倒置链表,效率远高于JDK1.7。

4. 代码验证:扩容后数据位置变化

import java.util.HashMap;

public class HashMapResizeDemo {
    public static void main(String[] args) {
        // 默认初始容量16,加载因子0.75,阈值12
        HashMap<Integer, String&gt; map = new HashMap<>();
        
        // 插入12个元素,触发第一次扩容(16→32)
        for (int i = 0; i < 12; i++) {
            map.put(i, "value" + i);
        }
        System.out.println("扩容前容量(实际数组长度):" + getCapacity(map));
        
        // 插入第13个元素,触发扩容
        map.put(12, "value12");
        System.out.println("扩容后容量(实际数组长度):" + getCapacity(map));
        
        // 验证数据位置(通过key的hash定位)
        int key = 5;
        int oldIndex = key.hashCode() & (16 - 1);
        int newIndex = key.hashCode() & (32 - 1);
        System.out.println("key=" + key + ",扩容前下标:" + oldIndex);
        System.out.println("key=" + key + ",扩容后下标:" + newIndex);
    }
    
    // 反射获取HashMap实际数组长度(仅用于验证,实际开发不推荐)
    private static int getCapacity(HashMap<?, ?> map) {
        try {
            java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
            field.setAccessible(true);
            Object[] table = (Object[]) field.get(map);
            return table.length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

运行结果:

扩容前容量(实际数组长度):16 扩容后容量(实际数组长度):32 key=5,扩容前下标:5 key=5,扩容后下标:5

结果说明:key=5的hash值新增二进制位为0,因此扩容后仍留在原下标5;若换一个key,可能会搬到5+16=21的位置。

二、第二问:HashMap默认容量是多少?

核心结论(面试一句话答完):HashMap默认初始容量是16,默认加载因子是0.75,且容量必须是2的幂

1. 核心要点(面试必背)

  1. 默认初始容量:1 << 4 = 16(JDK源码中定义为static final int DEFAULT_INITIAL_CAPACITY = 16);

  2. 容量约束:无论初始容量传入多少,HashMap都会自动调整为最近的2的幂(比如传入10,会调整为16;传入20,会调整为32);

  3. 为什么是2的幂?核心是为了配合下标计算公式i = hash & (length - 1),用位运算代替取模,计算更快,且能保证下标分布均匀,减少哈希冲突;

  4. 扩容触发时机:当HashMap中的元素个数(size)达到“容量×加载因子”时,触发扩容(默认16×0.75=12,插入第12个元素后,下一次插入会扩容)。

2. 代码验证:默认容量和扩容时机

import java.util.HashMap;

public class HashMapDefaultCapacityDemo {
    public static void main(String[] args) {
        // 无参构造,默认容量16,加载因子0.75
        HashMap&lt;String, Integer&gt; map = new HashMap<>();
        
        // 打印初始容量(通过反射获取)
        System.out.println("默认初始容量:" + getCapacity(map));
        
        // 插入12个元素,此时未触发扩容(size=12,等于阈值)
        for (int i = 0; i < 12; i++) {
            map.put("key" + i, i);
        }
        System.out.println("插入12个元素后,容量:" + getCapacity(map));
        
        // 插入第13个元素,触发扩容(16→32)
        map.put("key12", 12);
        System.out.println("插入13个元素后,容量:" + getCapacity(map));
    }
    
    // 反射获取实际容量
    private static int getCapacity(HashMap<?, ?> map) {
        try {
            java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
            field.setAccessible(true);
            Object[] table = (Object[]) field.get(map);
            return table.length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

运行结果:

默认初始容量:16 插入12个元素后,容量:16 插入13个元素后,容量:32

三、第三问:插入100个key,会扩容几次?

核心结论(默认配置下,面试直接答):默认HashMap(初始容量16,加载因子0.75)插入100个key,会扩容3次,最终容量变为128

1. 详细计算过程(面试可分步说)

扩容的核心逻辑:每次扩容后容量变为原来的2倍,扩容阈值=当前容量×0.75,当元素个数达到阈值时,下一次插入触发扩容。

具体步骤:

  1. 初始状态:容量=16,阈值=16×0.75=12;插入第12个元素后,下一次插入(第13个)触发第一次扩容,容量变为32;

  2. 第一次扩容后:容量=32,阈值=32×0.75=24;插入第24个元素后,下一次插入(第25个)触发第二次扩容,容量变为64;

  3. 第二次扩容后:容量=64,阈值=64×0.75=48;插入第48个元素后,下一次插入(第49个)触发第三次扩容,容量变为128;

  4. 第三次扩容后:容量=128,阈值=128×0.75=96;插入第96个元素后,下一次插入(第97个)才会触发第四次扩容,而我们只插入100个,因此不会再扩容。

总结扩容过程:16 → 32 → 64 → 128,共3次。

2. 代码验证:插入100个key的扩容次数

import java.util.HashMap;

public class HashMapResizeCountDemo {
    public static void main(String[] args) {
        HashMap&lt;Integer, String&gt; map = new HashMap<>();
        int resizeCount = 0;
        int prevCapacity = getCapacity(map);
        
        // 插入100个key
        for (int i = 0; i < 100; i++) {
            map.put(i, "value" + i);
            int currentCapacity = getCapacity(map);
            // 容量变化,说明触发扩容
            if (currentCapacity != prevCapacity) {
                resizeCount++;
                prevCapacity = currentCapacity;
            }
        }
        
        System.out.println("插入100个key后,最终容量:" + prevCapacity);
        System.out.println("扩容次数:" + resizeCount);
    }
    
    // 反射获取实际容量
    private static int getCapacity(HashMap<?, ?> map) {
        try {
            java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
            field.setAccessible(true);
            Object[] table = (Object[]) field.get(map);
            return table.length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

运行结果:

插入100个key后,最终容量:128 扩容次数:3

四、第四问:HashMap的get方法怎么工作?(JDK1.8)

核心结论(面试一句话总结):get方法的核心是“hash定位下标→查找桶→匹配key→返回结果”,先通过key计算hash,再定位数组下标,最后遍历链表/红黑树查找目标key

1. get方法完整流程(面试分步说)

假设执行map.get(key),完整步骤如下:

  1. 计算key的hash值:hash = key.hashCode() ^ (key.hashCode() >>> 16); 核心作用:将key的hashCode高位与低位异或,让高位也参与运算,减少哈希冲突,让下标分布更均匀。

  2. 计算数组下标:i = hash & (数组长度 - 1); 通过位运算快速定位到对应的桶(数组下标),比取模运算更快。

  3. 查找桶内元素:

  • 若桶的第一个节点(table[i])为空,直接返回null;

  • 若第一个节点的key与目标key匹配,直接返回该节点的value;

  • 若不匹配,判断桶内结构:是红黑树则按树结构查找,是链表则从头遍历查找。

  1. key匹配规则:key == targetKey || (key != null && key.equals(targetKey)); 先比较地址(==),再比较内容(equals),先快后慢,提升查找效率。

  2. 查找结果:找到匹配的key,返回对应的value;未找到,返回null。

2. 代码验证:get方法工作流程

import java.util.HashMap;

public class HashMapGetDemo {
    public static void main(String[] args) {
        HashMap&lt;String, String&gt; map = new HashMap<>();
        // 插入测试数据
        map.put("name", "张三");
        map.put("age", "20");
        map.put("gender", "男");
        
        // 测试get方法
        String name = map.get("name");
        String address = map.get("address");
        
        System.out.println("get("name"): " + name); // 输出:张三
        System.out.println("get("address"): " + address); // 输出:null
        
        // 验证hash计算和下标定位
        String key = "name";
        int hashCode = key.hashCode();
        int hash = hashCode ^ (hashCode >>> 16);
        int index = hash & (map.size() > 0 ? getCapacity(map) - 1 : 0);
        System.out.println("key="name"的hash值:" + hash);
        System.out.println("对应的数组下标:" + index);
    }
    
    // 反射获取实际容量
    private static int getCapacity(HashMap<?, ?> map) {
        try {
            java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
            field.setAccessible(true);
            Object[] table = (Object[]) field.get(map);
            return table.length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

运行结果:

get("name"): 张三 get("address"): null key="name"的hash值:3373707 对应的数组下标:7

3. 面试高频易错点澄清

  1. 误区:get方法先比较equals,再比较==? 错误:先比较==(地址),再比较equals(内容),因为==运算速度更快,能提升查找效率。

  2. 误区:hash值直接用key的hashCode? 错误:会将hashCode右移16位后与自身异或,目的是让高位参与运算,减少哈希冲突。

  3. 误区:用hash%length计算下标? 错误:用hash & (length-1)代替取模,位运算比取模运算更快,且在length是2的幂时,两者结果等价。

五、面试答题模板(直接背诵,稳拿高分)

针对这四个高频问题,面试时按以下逻辑答题,条理清晰、重点突出:

  1. 扩容数据变化:扩容2倍后,数组长度翻倍,数据要么留原下标,要么去原下标+旧容量,链表拆成两条,红黑树按规则拆分;

  2. 默认容量:默认16(1<<4),加载因子0.75,容量必须是2的幂,为了位运算高效定位下标;

  3. 插入100个key扩容次数:默认配置下3次,扩容过程16→32→64→128,阈值分别为12、24、48;

  4. get方法原理:先算hash(hashCode异或右移16位),再用hash&(len-1)定位下标,先比第一个节点,不匹配则遍历链表/红黑树,用==和equals匹配key,找到返回value,否则返回null。

六、面试加分金句(记住即可,瞬间拔高档次)

  1. HashMap扩容必须是2的幂,核心是为了让hash & (length-1)等价于取模,提升下标计算效率;

  2. JDK1.8扩容时链表拆分无需重新计算hash,仅根据新增二进制位拆分,效率极高,且不会倒置链表;

  3. get方法的查找效率:红黑树O(logn),链表O(n),因此HashMap会在链表长度超过8时转为红黑树,提升查找性能。

总结

本文围绕HashMap的四个高频面试问题,明确了核心结论和底层逻辑:扩容2倍时数据仅在两个位置分布,默认容量16且为2的幂,插入100个key扩容3次,get方法通过hash定位+链表/红黑树查找实现。理解这些核心知识点,不仅能轻松应对面试提问,更能在实际开发中合理使用HashMap,避免因不了解底层逻辑导致的性能问题。