深入解析HashMap:30道经典面试题带你彻底搞懂

张开发
2026/4/10 3:19:27 15 分钟阅读

分享文章

深入解析HashMap:30道经典面试题带你彻底搞懂
深入解析HashMap30道经典面试题带你彻底搞懂HashMap是Java面试中的“常客”无论是初级还是高级开发工程师HashMap相关的问题几乎都会出现在面试中。本文将汇总最经典的HashMap面试题从基础原理到源码分析帮助你在面试中游刃有余。目录基础概念篇源码深度篇并发安全篇实战经验篇性能优化篇基础概念篇1. 什么是HashMap它的底层数据结构是什么答HashMap是基于哈希表的Map接口实现以key-value形式存储数据。JDK 1.8之前底层采用数组链表的结构JDK 1.8及以后采用数组链表红黑树的结构。当链表长度超过阈值默认为8且数组长度大于64时链表会转换为红黑树以提高查询效率。2. HashMap和Hashtable有什么区别比较维度HashMapHashtable线程安全非线程安全线程安全synchronized允许nullkey和value都允许null不允许null初始容量1611扩容机制2倍扩容2倍1扩容继承关系AbstractMapDictionary已废弃3. 什么是哈希冲突HashMap如何解决哈希冲突答哈希冲突指不同的key经过hash函数计算后得到相同的数组下标位置。HashMap采用链地址法解决哈希冲突当发生冲突时将新节点插入到对应下标的链表中JDK 1.8之前采用头插法JDK 1.8后采用尾插法当链表长度超过阈值转换为红黑树优化查询性能源码深度篇4. HashMap的put方法执行流程是怎样的// 简化版put流程1.计算key的hash值hash(keynull)?0:(hkey.hashCode())^(h16)2.检查table数组是否初始化未初始化则resize()3.计算下标i(n-1)hash4.如果该位置为空直接插入新节点5.如果该位置不为空-如果key相同直接覆盖value-如果是红黑树节点插入红黑树-如果是链表节点遍历链表*找到相同key则覆盖*未找到则在链表尾部插入新节点*检查链表长度是否超过阈值(8)超过则树化6.检查size是否超过threshold超过则resize()5. 为什么HashMap的容量必须是2的幂次方主要原因为了高效计算数组下标和减少哈希冲突。// 计算下标的代码index(n-1)hash// n为数组长度如果n是2的幂次方n-1的二进制形式全是1与hash做与运算时结果取决于hash的低位分布更均匀如果n不是2的幂次方n-1的二进制包含0与运算会屏蔽部分hash位增加冲突概率位运算()比取模运算(%)效率更高6. HashMap的hash函数是如何设计的为什么要这样设计staticfinalinthash(Objectkey){inth;return(keynull)?0:(hkey.hashCode())^(h16);}设计目的让高位也参与下标计算减少哈希冲突。原理获取key的hashCode()32位int值将hashCode的高16位与低16位进行异或运算这样在数组长度较小时高位特征也能保留下来例子假设n16n-115(二进制00001111)如果不做扰动直接使用hashCode只有低4位参与运算扰动后高位特征混入低16位分布更均匀7. 什么是加载因子为什么默认是0.75答加载因子是HashMap在扩容前允许达到的填充程度计算公式threshold capacity * loadFactor。默认值0.75的原因时间和空间的平衡折中。加载因子过高如1空间利用率高但哈希冲突增加查询效率降低加载因子过低如0.5哈希冲突减少但空间浪费大频繁扩容0.75是统计学得出的较优值泊松分布下链表长度达到8的概率极小8. 为什么JDK 1.8要将链表转换为红黑树的阈值设为8答基于时间和空间复杂度的权衡以及泊松分布的计算。根据泊松分布在加载因子为0.75的情况下哈希桶中链表长度的概率长度为00.60653066长度为10.30326533长度为20.07581633长度为30.01291061长度为40.00160452长度为50.00014379长度为60.00001145长度为70.00000094长度为80.00000006当链表长度达到8时概率已经极低千万分之六此时转为红黑树既不会太频繁也能应对极端情况下的性能问题。9. HashMap的扩容机制是怎样的// 扩容关键步骤1.新容量旧容量12倍扩容2.新阈值新容量*加载因子3.创建新数组2倍大小4.重新计算每个元素的位置-如果e.hasholdCap0元素位置不变原索引-否则新位置原索引oldCap为什么可以这样计算因为容量是2的幂次方扩容后n-1比原来多了一个高位1通过hash值的新增高位是0还是1可以快速判断元素的新位置避免了JDK 1.7中重新计算hash的损耗10. JDK 1.8中HashMap做了哪些优化优化点JDK 1.7JDK 1.8数据结构数组链表数组链表红黑树插入方式头插法尾插法hash计算多次扰动1次异或运算扩容重定位重新hash直接判断高位链表插入容易形成死循环解决死循环问题并发安全篇11. HashMap是线程安全的吗为什么答HashMap不是线程安全的。在多线程环境下会出现以下问题JDK 1.7扩容时头插法可能导致循环链表引起CPU 100%JDK 1.8虽然解决了循环链表问题但仍存在数据覆盖问题如put时两个线程同时检测到空桶都会插入数据12. 什么是ConcurrentHashMap它如何保证线程安全答ConcurrentHashMap是线程安全的HashMap实现。JDK 1.7实现分段锁(Segment)默认16个Segment每个Segment继承ReentrantLock理论上支持16个线程并发写入锁粒度较粗JDK 1.8实现CAS synchronized放弃分段锁使用Node数组CASsynchronized锁粒度更细锁单个桶并发度更高13. 如何在多线程环境下使用HashMap方案一使用Collections.synchronizedMap()MapmapCollections.synchronizedMap(newHashMap());方案二使用Collections.synchronizedMap()MapmapnewConcurrentHashMap();方案三使用Collections.synchronizedMap()MapmapnewHashtable();推荐优先使用ConcurrentHashMap并发性能最好。14. 为什么JDK 1.7的HashMap在多线程扩容时会出现死循环原因头插法导致链表反转时形成环。场景线程1和线程2同时扩容线程1执行到一半被挂起线程2完成扩容后链表顺序反转。线程1恢复后引用关系混乱形成循环链表导致get操作陷入死循环。解决JDK 1.8改为尾插法保持链表原有顺序避免循环链表。实战经验篇15. 如果使用自定义对象作为HashMap的key需要注意什么必须重写hashCode()和equals()方法publicclassPerson{privateStringid;privateStringname;Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;Personperson(Person)o;returnObjects.equals(id,person.id);}OverridepublicinthashCode(){returnObjects.hash(id);}}原则两个对象equals相等hashCode必须相等两个对象hashCode相等equals不一定相等16. HashMap中key为null时存储在哪里答存储在table[0]的位置。// HashMap的hash方法staticfinalinthash(Objectkey){inth;// key为null时hash值为0return(keynull)?0:(hkey.hashCode())^(h16);}17. 如何遍历HashMap哪种方式效率最高// 方式1使用entrySet推荐for(Map.EntryString,Stringentry:map.entrySet()){Stringkeyentry.getKey();Stringvalueentry.getValue();}// 方式2使用keySet getfor(Stringkey:map.keySet()){Stringvaluemap.get(key);// 多了一次查询}// 方式3使用iteratorIteratorMap.EntryString,Stringiteratormap.entrySet().iterator();while(iterator.hasNext()){Map.EntryString,Stringentryiterator.next();}// 方式4Java 8 forEachmap.forEach((key,value)-{System.out.println(key:value);});效率对比entrySet ≈ forEach keySetkeySet需要多一次get查询18. HashMap和LinkedHashMap有什么区别特性HashMapLinkedHashMap顺序无序可预测的迭代顺序插入顺序或访问顺序性能略高略低需要维护双向链表实现数组链表红黑树继承HashMap增加双向链表用途通用场景LRU缓存、需要有序的场景LRU缓存示例// 构建LRU缓存LinkedHashMapString,StringlruCachenewLinkedHashMapString,String(16,0.75f,true){OverrideprotectedbooleanremoveEldestEntry(Map.EntryString,Stringeldest){returnsize()10;// 最多缓存10个}};19. HashMap和TreeMap有什么区别特性HashMapTreeMap顺序无序自然顺序或自定义顺序底层结构哈希表红黑树时间复杂度O(1)O(log n)允许nullkey可为nullkey不能为null会抛异常接口MapMap SortedMap性能优化篇20. 如何初始化HashMap才能避免频繁扩容估算初始容量// 需要存储100个元素加载因子0.75// 容量 100 / 0.75 ≈ 134取2的幂次方256MapmapnewHashMap(256);计算公式初始容量 (需要存储的元素数量 / 加载因子) 1计算方法初始容量 (需要存储的元素数量 / 加载因子) 1工具方法:MapString,StringmapnewHashMap((int)(100/0.75f)1);// 或者使用GuavaMapString,StringmapMaps.newHashMapWithExpectedSize(100);21. HashMap的查询时间复杂度是多少最好情况O(1) - 无哈希冲突最坏情况O(log n) - 全部在红黑树中平均情况接近O(1) - 哈希函数分布均匀22. 为什么重写equals时必须重写hashCode反例publicclassStudent{privateStringid;// 只重写equals没重写hashCodepublicbooleanequals(Objectobj){// ...}}MapStudent,StringmapnewHashMap();map.put(newStudent(001),张三);map.get(newStudent(001));// 返回null原因HashMap先比较hashCode如果hashCode不同直接认为key不同不会调用equals。两个对象equals相等但hashCode不同会导致无法从HashMap中获取值。23. String类型的key有什么优势答String作为HashMap的key有以下优势不可变性保证了hashCode不会变化hashCode缓存String内部缓存了hashCode计算一次后重复使用良好的hash分布String的hash算法经过优化冲突较少24. HashMap的key一般使用什么类型推荐使用不可变类StringInteger、Long等包装类自定义不可变类所有字段final避免使用可变对象作为key否则修改对象后会导致无法找到原来的value。25. 如何计算HashMap占用的内存大小粗略估算每个Entry对象约32字节对象头key引用value引用hashnext空数组16 * 引用大小8字节 128字节存储n个元素约 n * 32 数组大小精确计算使用Java对象内存计算工具如jol-core26. 什么是扰动函数为什么需要它答扰动函数是指将高位特征混合到低位的操作如HashMap中的hash ^ (hash 16)。目的当数组长度较小时让高位也参与下标计算使数据分布更均匀减少冲突。类比就像将一桶水的上层和下层的混合后再倒出避免只有底层的水被使用。27. 红黑树在HashMap中起到什么作用作用优化极端情况下的查询性能。正常情况下链表查询时间复杂度O(n)链表长度超过8时查询效率下降转换为红黑树后查询时间复杂度降为O(log n)节点数小于6时会转换回链表避免频繁转换28. HashMap的fail-fast机制是什么答HashMap的迭代器采用fail-fast机制当多个线程同时修改HashMap时会抛出ConcurrentModificationException。MapString,StringmapnewHashMap();Iteratoritmap.entrySet().iterator();map.put(key,value);// 修改操作while(it.hasNext()){// 抛出ConcurrentModificationExceptionit.next();}原理HashMap内部维护modCount变量记录修改次数。每次迭代时比较modCount是否变化。29. 什么是容量、大小和阈值容量(capacity)哈希表数组的长度默认为16大小(size)HashMap中实际存储的键值对数量阈值(threshold)允许存储的最大键值对数量capacity * loadFactor超过阈值需要扩容30. 手写一个简单的HashMap实现publicclassSimpleHashMapK,V{privateNodeK,V[]table;privateintsize;privatestaticfinalintDEFAULT_CAPACITY16;staticclassNodeK,V{finalinthash;finalKkey;Vvalue;NodeK,Vnext;Node(inthash,Kkey,Vvalue,NodeK,Vnext){this.hashhash;this.keykey;this.valuevalue;this.nextnext;}}publicSimpleHashMap(){tablenewNode[DEFAULT_CAPACITY];}privateinthash(Objectkey){inth;return(keynull)?0:(hkey.hashCode())^(h16);}publicvoidput(Kkey,Vvalue){inthashhash(key);intindex(table.length-1)hash;if(table[index]null){table[index]newNode(hash,key,value,null);}else{NodeK,Vnodetable[index];NodeK,Vprevnull;while(node!null){if(node.hashhash(node.keykey||(key!nullkey.equals(node.key)))){node.valuevalue;return;}prevnode;nodenode.next;}prev.nextnewNode(hash,key,value,null);}size;}publicVget(Kkey){inthashhash(key);intindex(table.length-1)hash;NodeK,Vnodetable[index];while(node!null){if(node.hashhash(node.keykey||(key!nullkey.equals(node.key)))){returnnode.value;}nodenode.next;}returnnull;}}总结HashMap是Java程序员必须掌握的核心知识。通过以上30道面试题我们深入了解了HashMap的底层原理数组链表红黑树的数据结构核心机制hash计算、下标定位、扩容重定位设计哲学空间与时间的权衡并发问题线程不安全的根源及解决方案最佳实践初始化容量、key的选择等掌握这些知识点不仅能应对面试更能帮助我们在实际开发中写出更高效、更健壮的代码。最后的小建议面试时不要死记硬背答案要理解背后的设计思想和权衡这样才能在面试官的追问下游刃有余。祝你面试成功

更多文章