拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【秋招必看】Java 集合面试热题(一)

【秋招必看】Java 集合面试热题(一)

目录

1.说说 Java 中 HashMap 的原理?

2.Java 中的 List 接口有哪些实现类?

3.Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?

4.为什么 JDK 1.8 对 HashMap 进行了红黑树的改动?

5.JDK 1.8 对 HashMap 除了红黑树还进行了哪些改动?

6.Java 中有哪些集合类?请简单介绍。

7.为什么 Java 中 HashMap 的默认负载因子是 0.75?

8.Java 中 HashMap 的扩容机制是怎样的?

9.为什么 HashMap 在 Java 中扩容时采用 2 的 n 次方倍?

10.数组和链表在 Java 中的区别是什么?


1.Java 中有哪些集合类?请简单介绍。(中)

(一般用于暖场,简单讲讲即可)

Java 集合框架(Java Collections Framework)是 Java 核心库中用于存储和操作数据集合的标准架构,主要分为Collection(单元素集合)和Map(键值对集合)两大接口体系。

Collection:

Map:

图片来源

1)Collection 接口(单元素集合)

List 接口(有序、可重复)

类名数据结构线程安全特点
ArrayList动态数组❌随机访问快,增删中间元素慢
LinkedList双向链表❌增删快,随机访问慢;实现了Deque接口
Vector动态数组✅线程安全但性能差(已过时)
Stack栈(继承Vector)✅后进先出(LIFO)
CopyOnWriteArrayList动态数组 + 写时复制✅读操作无锁,写操作复制整个数组(适合读多写少)

Set 接口(无序、唯一)

类名数据结构线程安全特点
HashSet哈希表❌基于HashMap实现,查询最快
LinkedHashSet哈希表 + 双向链表❌保持插入顺序
TreeSet红黑树❌元素自动排序(需实现Comparable)
CopyOnWriteArraySet数组 + 写时复制✅基于CopyOnWriteArrayList
ConcurrentSkipListSet跳表✅有序且线程安全(高并发场景)

Queue 接口(队列)

类名数据结构线程安全特点
LinkedList双向链表❌作为队列使用,支持FIFO操作
PriorityQueue堆(小顶堆)❌元素按优先级排序(自然序或自定义比较器)
ArrayDeque循环数组❌双端队列,性能优于LinkedList
ConcurrentLinkedQueue链表✅无锁并发队列(CAS 实现)

2)Map 接口(键值对集合)

类名数据结构线程安全特点
HashMap数组 + 链表/红黑树❌最常用,允许null键/值
LinkedHashMap哈希表 + 双向链表❌保持插入顺序或访问顺序(LRU 缓存基础)
TreeMap红黑树❌键自动排序(基于红黑树)
Hashtable哈希表✅过时,被ConcurrentHashMap取代
ConcurrentHashMap数组 + 链表/红黑树 + CAS✅高并发推荐,分段锁/桶锁优化

3)如何选择

  1. 需要键值对→HashMap(非并发)、ConcurrentHashMap(并发)

  2. 需要有序→LinkedHashMap(插入序)、TreeMap(排序)

  3. 去重存储→HashSet

  4. 队列/栈→ArrayDeque(双端队列)、LinkedList(栈)

  5. 高并发场景→ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue

  6. 排序需求→TreeSet、TreeMap、PriorityQueue

4)常用声明

声明(左)实现(右)场景
List<T>new ArrayList<>()一般列表
List<T>new LinkedList<>()频繁头插/删除
Set<T>new HashSet<>()去重
Map<K,V>new HashMap<>()键值映射
Queue<T>new ArrayDeque<>()队列
Deque<T>new ArrayDeque<>()栈 / 双端队列
Deque<T>new LinkedList<>()需要存null时

规律:左边永远是接口(或抽象父类),右边是具体实现。

2.Java 中的 List 接口有哪些实现类?(易)

Java 中的 List 接口是 Collection(单元素容器) 的子接口,表示有序、可重复的集合。常见的实现类包括:ArrayList, LinkedList, Vector, Stack, CopyOnWriteArrayList几个实现类。

实现类底层结构线程安全适用场景
ArrayList动态数组❌随机访问多,增删少
LinkedList双向链表❌频繁增删,少量随机访问
Vector动态数组✔️(synchronized)遗留代码,不推荐新项目
Stack继承Vector✔️栈结构(推荐Deque替代)
CopyOnWriteArrayList动态数组(COW)✔️高并发读,低并发写

3.说说 Java 中 HashMap 的原理?(中)

  1. HashMap基于数组+链表/红黑树实现。
  2. 存储键值对时,先计算Key的hashCode,再扰动处理,然后(n-1)&hash确定桶位置。
  3. 如果桶为空直接放;如果桶不为空,则遍历链表/树用equals比较Key:存在则覆盖Value,不存在则添加新节点(尾插法)。
  4. 当链表长度>=8且数组长度>=64时链表转红黑树;树节点数<=6时树退化为链表。
  5. 元素总数超过容量*负载因子(默认0.75)时会扩容(通常2倍)并重新哈希。
  6. 查询时类似定位桶,再遍历链表/树用equals查找。
  7. 它允许null键值、无序、非线程安全,理想情况下操作时间复杂度是O(1)。

(参考AI)

核心原理:

  1. 基于哈希表的键值对存储:

    • 使用数组(称为桶或bucket)作为主干来存储数据。

    • 每个数组元素通常是一个链表的头节点(Java 8后可能变为红黑树)。

  2. put操作(存储键值对):

    • 计算哈希值:调用键(Key)对象的hashCode()方法计算其哈希值。

    • 计算桶下标:对哈希值进行特定的扰动计算(Java 8使用(h = key.hashCode()) ^ (h >>> 16)来减少碰撞),然后通过(数组长度 - 1) & hash(等价于hash % 数组长度,但效率更高)确定键值对应该存储在哪个桶(数组索引)。

    • 处理碰撞(哈希冲突):

      • 如果目标桶为空:直接创建一个新节点(包含Key, Value, hash)放入该桶。

      • 如果目标桶不为空(发生碰撞):

        • 链表:遍历桶中的链表(或树),用equals()方法比较新Key和链表中每个节点的Key:

          • 如果找到相等的Key:用新Value覆盖旧Value。

          • 如果没找到相等的Key:将新节点添加到链表末尾(Java 7是头插法,Java 8改为尾插法)。

        • 树化:当链表长度超过阈值(默认=8)且数组总长度达到一定大小(默认>=64)时,该链表会转换为红黑树(TreeNode),以提高长链表下的查询效率(O(n) -> O(log n))。

    • 扩容:如果添加元素后,整个HashMap中元素的数量(size)超过了数组长度 * 负载因子(默认负载因子loadFactor=0.75),则触发扩容(resize):

      • 创建一个新的、更大的数组(通常是原长度的2倍)。

      • 重新哈希:遍历所有旧的桶和链表/树,根据新的数组长度重新计算每个节点的桶下标,并将节点迁移到新数组中。

      • 树退化:在迁移过程中,如果树中元素数量减少到阈值以下(默认<=6),红黑树会退化为链表。

  3. get操作(根据键取值):

    • 计算哈希值 & 桶下标:与put操作相同的方式计算Key的哈希值和桶下标。

    • 遍历链表/树:

      • 如果目标桶为空:返回null。

      • 如果目标桶不为空:

        • 如果桶中第一个节点(链表头或树根)的Key匹配(equals):直接返回其Value。

        • 否则,遍历该桶上的链表或红黑树,用equals()方法比较查找的Key和节点的Key:

          • 找到匹配的Key:返回对应Value。

          • 遍历完未找到:返回null。

  4. 关键特性:

    • 无序:迭代顺序不保证与插入顺序一致,也不保证顺序不变。

    • 允许null键和null值。

    • 非线程安全:多线程环境下并发修改可能导致死循环(Java 7头插法导致)、数据错乱或ConcurrentModificationException。需要外部同步(如Collections.synchronizedMap)或使用ConcurrentHashMap。

    • 性能:在理想情况下(无碰撞或碰撞少),get和put操作的时间复杂度接近O(1)。最坏情况(所有键都碰撞到同一个桶,退化为链表)是O(n),树化后提升为O(log n)。

4.Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?(中)

Java 7 的 ConcurrentHashMap 采用分段锁(Segment)实现,默认16个段,每个段独立加锁,允许并发写入不同段,但扩容和哈希冲突仍受段限制。Java 8 则抛弃分段锁,改用CAS + synchronized对单个桶(Node)加锁,并引入红黑树优化哈希冲突,扩容时支持多线程协同迁移,并发度更高且内存开销更小。此外,Java 8 新增函数式 API(如 forEach、compute),并优化了统计方法(如size()使用 CounterCell 分散计数竞争)。1.8 的实现更简洁高效,锁粒度更细,适应更高并发场景。

特性JDK 1.7JDK 1.8
数据结构Segment + HashEntry (数组+链表)Node 数组 + 链表/红黑树
锁粒度Segment 级别(粗粒度)桶级别(细粒度)
锁实现ReentrantLockCAS + synchronized
哈希冲突处理链表(O(n))链表转红黑树(O(log n))
扩容各 Segment 独立扩容多线程协同迁移数据
适用场景写少读多高并发写入、大数据量

5.为什么 JDK 1.8 对 HashMap 进行了红黑树的改动? (中)

JDK 1.8 在 HashMap 中引入红黑树,主要是为了解决哈希冲突严重时长链表导致的查询性能退化(O(n))问题。当单个桶的链表长度>=8且数组长度>=64时,链表会转换为红黑树,将最坏情况下的操作时间复杂度从O(n)优化到O(log n),显著提升了高冲突场景下的性能和容器的抗攻击(防哈希碰撞DoS)能力。当链表长度<=6时,红黑树会重新退化为链表,保证低冲突时链表的效率优势。

数据结构查找时间复杂度插入时间复杂度适用场景
链表O(n)O(1)冲突较少
红黑树O(log n)O(log n)冲突严重
  • 为什么阈值是8?

    • 根据泊松分布,哈希冲突达到8的概率小于千万分之一

    • 树节点占用空间是普通节点的两倍,平衡性能与开销

  • 为什么需要最小树化容量64?

    • 避免早期小规模哈希表的不必要树化

    • 优先通过数组扩容分散节点

6.JDK 1.8 对 HashMap 除了红黑树还进行了哪些改动? (中)

1)哈希函数优化

  • 扰动算法升级:计算索引时,新增一步hash = key.hashCode() ^ (key.hashCode() >>> 16),将高16位与低16位异或混合,显著减少哈希冲突,使元素分布更均匀2410。

2)链表插入方式改变

  • 头插法 → 尾插法:JDK 1.7 使用头插法(易导致多线程扩容死循环),1.8 改为尾插法,避免链表倒置,提升并发安全性(尽管仍非线程安全)710。

3)扩容机制重构

  • 位置重计算优化:扩容时不再全量重新哈希,而是通过(e.hash & oldCap) == 0判断元素位置:

    • 若为0,索引不变;

    • 若为1,新索引 = 原索引 + 旧容量479。

  • 效率提升:避免了重新计算哈希,仅需一次位操作,扩容性能大幅提高。

4)树化条件精细化

  • 链表转红黑树需同时满足:

    • 链表长度 ≥TREEIFY_THRESHOLD(默认8);

    • 桶数组容量 ≥MIN_TREEIFY_CAPACITY(默认64)。

  • 否则优先扩容而非树化,避免小表不必要的树结构开销569。

5)并发性能增强

  • 虽仍非线程安全,但内部实现采用CAS 思想(如size统计通过CounterCell分散竞争),减少锁冲突12。

特性JDK 1.7JDK 1.8
数据结构数组 + 链表数组 + 链表 + 红黑树
哈希计算直接取模高位扰动后取模
插入方式头插法尾插法
扩容开销全量重哈希位运算判断新位置
树化逻辑无长度 ≥8 且容量 ≥64 才树化

7.为什么 Java 中 HashMap 的默认负载因子是 0.75?(中)

Java 中HashMap的默认负载因子(Load Factor)设为0.75是经过严谨权衡的结果,主要目的是在时间效率(查询性能)和空间效率(内存利用率)之间取得最佳平衡。既保障操作效率,又避免内存浪费,适合绝大多数场景。

负载因子查询性能内存利用率适用场景
0.5✅ 极高(冲突少)❌ 低(浪费 50%)内存充足、实时系统
0.75✅平衡(接近 O(1))✅ 较高(闲置 25%)默认场景
1.0❌ 差(冲突频繁)✅ 高(无浪费)内存紧张、低频访问

8.Java 中 HashMap 的扩容机制是怎样的?(中)

HashMap 在元素数超过容量×0.75时触发扩容,新容量为当前容量的两倍。JDK 1.8 后通过高位比特判断节点新位置(免重算哈希),用尾插法拆分链表/树迁移数据,避免死循环并提升效率。

具体步骤:

  1. 创建新数组:
    新容量 =旧容量的 2 倍(如 16 → 32),保证容量始终为 2 的幂(便于位运算优化)。

  2. 迁移数据:
    遍历旧数组的每个桶(Bucket),重新分配每个节点到新数组:

    • JDK 1.8 优化关键:通过e.hash & oldCap(原数组长度的二进制数)高位比特判断位置(无需重算哈希,如16为10000,看 e.hash 第五位是否为1):

      • 结果为 0→ 节点留在原索引位置(index不变)。

      • 结果为 1→ 节点迁移到新索引 = 原索引 + 旧容量(index + oldCap)。

    • 链表拆分:若桶中是链表,按高位结果拆分为两个链表(原位置链表 + 新位置链表),保持顺序(尾插法)。

    • 红黑树拆分:若桶中是红黑树,按相同逻辑拆分,若拆分后节点数 ≤6,则退化为链表。

  3. 更新引用:
    将新数组设置为HashMap的底层存储,旧数组被 GC 回收。

  4. 重新计算阈值:
    新阈值 = 新容量 × 负载因子(如 32 × 0.75 = 24)。

操作JDK 1.7JDK 1.8
哈希重计算所有节点重新计算hash免重算(用e.hash & oldCap判断)
链表迁移头插法(可能死循环)尾插法(避免闭环)
迁移效率单节点遍历迁移按高位结果批量迁移链表/树

9.为什么 HashMap 在 Java 中扩容时采用 2 的 n 次方倍?(中)

HashMap 采用 2 的 n 次方容量,核心是通过(n-1) & hash位运算替代取模,极大提升计算桶下标的效率;同时支持扩容时免重算哈希(仅需高位比特判断新位置n & hash),降低迁移开销,并提升哈希分布的均匀性,是性能与设计优雅性的双重优化。

具体分析:

  1. 高效计算桶下标(核心优化)

    • 定位桶下标公式:index = (n - 1) & hash(n为数组长度)。

    • 当n为 2 的幂时:n - 1的二进制全为1(例如16-1=15 → 1111)。

    • 位运算替代取模:(n-1) & hash等价于hash % n,但位运算比取模快 10 倍以上(CPU 指令级优化)。

  2. 扩容时免重算哈希(JDK 1.8 优化)

    • 扩容后新下标 =原位置或原位置 + 旧容量(index或index + oldCap)。

    • 判断逻辑:直接通过e.hash & oldCap的高位比特(0 或 1)决定位置,无需重新计算hash值。

    • 例如:旧容量16(二进制10000),若e.hash & 16 = 0则位置不变,否则新位置 = 原位置 + 16。

  3. 减少哈希冲突,分布更均匀

    • 如果length不是 2 的幂次方,(length - 1)的二进制会有0位(如length = 15→1110),导致某些index永远无法被计算到(如0001),增加哈希冲突概率。

    • 2 的幂次方长度能更均匀分布元素((n-1) & hash),确保哈希值的所有有效位都参与计算,提高查询效率。

特性2^n 容量非 2^n 容量
计算桶位置速度1 CPU 周期10+ CPU 周期
哈希分布均匀性最优(全低位参与)部分桶位不可用
扩容元素迁移位判断(O(1))全量重哈希(O(n))
内存利用率100% 有效最高 87.5%

10.数组和链表在 Java 中的区别是什么?(中)

数组基于连续的内存块,且大小固定,支持O(1) 随机访问但增删成本高;链表基于节点,通过指针动态链接,增删 O(1)但访问需 O(n)。在 Java 中,ArrayList基于数组适合读多写少,LinkedList基于链表适合频繁增删场景。

特性数组链表
内存结构连续内存块非连续内存(节点分散存储)
访问效率O(1)(通过下标直接寻址)O(n)(需从头遍历)
增删效率O(n)(需移动后续元素)O(1)(仅修改指针,无需移动)
内存占用固定大小(初始化后不可变)动态扩容(按需增删节点)
内存开销仅存储数据额外存储指针(next/prev)
适用场景频繁随机访问、数据量固定频繁增删、数据量动态变化

本文到此结束,如果对你有帮助,可以点个赞~

后续会在合集里持续更新 Java 相关的面试题,欢迎关注~

祝各位都能拿到满意的offer~

返回列表