云计算百科
云计算领域专业知识百科平台

【秋招必看】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)如何选择

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

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

  • 去重存储 → HashSet

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

  • 高并发场景 → ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue

  • 排序需求 → TreeSet、TreeMap、PriorityQueue

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

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

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

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

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

    核心原理:

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

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

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

  • 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),红黑树会退化为链表。

  • get操作(根据键取值):

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

    • 遍历链表/树:

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

      • 如果目标桶不为空:

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

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

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

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

  • 关键特性:

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

    • 允许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 级别(粗粒度) 桶级别(细粒度)
    锁实现 ReentrantLock CAS + 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 后通过 高位比特判断节点新位置(免重算哈希),用 尾插法拆分链表/树 迁移数据,避免死循环并提升效率。

    具体步骤:

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

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

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

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

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

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

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

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

  • 重新计算阈值: 新阈值 = 新容量 × 负载因子(如 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 ),降低迁移开销,并提升哈希分布的均匀性,是性能与设计优雅性的双重优化。

    具体分析:

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

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

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

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

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

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

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

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

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

    • 如果 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~

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【秋招必看】Java 集合面试热题(一)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!