Java 集合框架 专属复习笔记

目录

模块一:集合整体架构

核心定义

个人理解

项目实际使用场景

面试考点标注

[模块二:List 体系(有序、可重复)](#模块二:List 体系(有序、可重复))

[1. ArrayList(项目最高频)](#1. ArrayList(项目最高频))

[2. LinkedList](#2. LinkedList)

[3. Vector 与 CopyOnWriteArrayList(线程安全 List)](#3. Vector 与 CopyOnWriteArrayList(线程安全 List))

个人理解

项目实际使用场景

面试考点标注

[模块三:Set 体系(无序、不可重复)](#模块三:Set 体系(无序、不可重复))

[1. HashSet](#1. HashSet)

核心定义

个人理解

项目实际使用场景

面试考点标注

[2. LinkedHashSet](#2. LinkedHashSet)

核心定义

项目实际使用场景

面试考点标注

[3. TreeSet](#3. TreeSet)

核心定义

项目实际使用场景

面试考点标注

[模块四:Map 体系(面试 100% 必问,重中之重)](#模块四:Map 体系(面试 100% 必问,重中之重))

[1. HashMap(面试核心中的核心)](#1. HashMap(面试核心中的核心))

核心定义

个人理解

项目实际使用场景

面试考点标注

[2. ConcurrentHashMap(线程安全 Map,面试必问)](#2. ConcurrentHashMap(线程安全 Map,面试必问))

核心定义

个人理解

项目实际使用场景

面试考点标注

[3. LinkedHashMap 与 TreeMap](#3. LinkedHashMap 与 TreeMap)

核心定义

项目实际使用场景

面试考点标注

当日验收清单

[Java 集合框架 面试模拟题(实习面试专属,附标准答案)](#Java 集合框架 面试模拟题(实习面试专属,附标准答案))

[一、基础必考题(8 道,中小厂实习 100% 覆盖)](#一、基础必考题(8 道,中小厂实习 100% 覆盖))

[1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?](#1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?)

[2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?](#2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?)

[3. ConcurrentHashMap 和 HashTable 的核心区别是什么?](#3. ConcurrentHashMap 和 HashTable 的核心区别是什么?)

[4. HashSet 的去重原理是什么?](#4. HashSet 的去重原理是什么?)

[5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?](#5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?)

[6. ArrayList 的扩容机制是什么?](#6. ArrayList 的扩容机制是什么?)

[7. 简述 HashMap 的 put () 方法完整执行流程(JDK8)](#7. 简述 HashMap 的 put () 方法完整执行流程(JDK8))

[8. Comparable 和 Comparator 的核心区别是什么?](#8. Comparable 和 Comparator 的核心区别是什么?)

[二、场景坑点题(6 道,大厂实习高频代码题 / 业务题)](#二、场景坑点题(6 道,大厂实习高频代码题 / 业务题))

[1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?](#1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?)

[2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?](#2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?)

[3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?](#3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?)

[4. 以下场景分别应该用什么 List 集合?为什么?](#4. 以下场景分别应该用什么 List 集合?为什么?)

[5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?](#5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?)

[6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。](#6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。)

[三、进阶原理题(4 道,中大厂实习拔高题)](#三、进阶原理题(4 道,中大厂实习拔高题))

[1. HashMap 的容量为什么必须是 2 的 n 次方?](#1. HashMap 的容量为什么必须是 2 的 n 次方?)

[2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?](#2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?)

[3. HashMap 的链表转红黑树的阈值为什么是 8?](#3. HashMap 的链表转红黑树的阈值为什么是 8?)

[4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?](#4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?)

实习面试答题加分技巧(绑定你的校园二手平台项目)


模块一:集合整体架构

核心定义

Java 集合分为两大顶层分支,所有集合都在java.util包下:

1. 两大顶层接口
顶层接口 特点 子分支
Collection 单列集合,存储单个元素 List(有序、可重复)、Set(无序、不可重复)、Queue(队列,先进先出)
Map 双列集合,存储 Key-Value 键值对,Key 不可重复 HashMapConcurrentHashMapTreeMapLinkedHashMap
2. 集合与数组的核心区别
对比维度 数组 集合
长度 固定,初始化必须指定长度 动态扩容,长度可变
存储类型 可以存基本类型、引用类型 只能存引用类型(基本类型需要自动装箱为包装类)
方法 只有 length 属性,无内置方法 封装了大量增删改查、遍历工具方法
性能 连续内存,性能极高 基于不同数据结构,性能各有差异
3. 迭代器 Iterator

是集合遍历的统一接口,核心设计是统一所有集合的遍历方式 ,屏蔽不同集合的底层实现差异,核心方法:hasNext()next()remove()

4. fail-fast(快速失败)机制

是集合的错误检测机制,遍历集合时,如果其他线程修改了集合的结构(add/remove),迭代器会立即抛出ConcurrentModificationException,核心实现是modCount计数:集合每次修改都会更新modCount,迭代器遍历时会对比expectedModCountmodCount,不一致就抛异常。


个人理解

集合本质是 Java 对常用数据结构的封装,不同集合对应不同的数据结构(数组、链表、红黑树、哈希表),开发中核心是根据业务场景选择合适的集合,而不是所有场景都用 ArrayList;Iterator 是迭代器模式的经典实现,也是 Java 面向抽象编程的体现。


项目实际使用场景

结合校园二手平台开发实践:

  1. 商品列表、订单列表 :用List存储,有序、可重复,支持随机查询;
  2. 用户浏览历史去重、用户角色权限去重 :用Set存储,自动去重;
  3. 商品缓存、用户 Session 缓存、接口限流计数 :用Map存储,Key-Value 结构,O (1) 时间复杂度查询;
  4. 遍历删除下架商品 :统一用Iterator遍历,避免并发修改异常。

面试考点标注

✅ 必问:Collection 和 Collections 的区别(Collection 是集合顶层接口,Collections 是集合工具类,提供排序、反转等静态方法);

✅ 原理:fail-fast 机制的实现原理;

  • fail-fast = 集合遍历安全检测机制

  • 依靠 modCount(集合修改次数)实现

  • 迭代器保存 expectedModCount 做对比

  • 不一致 → 抛 ConcurrentModificationException

  • 集合.remove () 报错,迭代器.remove () 不报错

✅ 细节:集合为什么不能存基本类型?(泛型擦除后都是 Object 类型,基本类型不是 Object 的子类)。


模块二:List 体系(有序、可重复)

1. ArrayList(项目最高频)

核心定义
  • 底层结构 :JDK8 及之后,底层是transient Object[] elementData动态数组,默认初始容量 10;
  • 扩容机制 :容量不足时,自动扩容为原容量的1.5 倍 (位运算oldCapacity + (oldCapacity >> 1)),扩容时会创建新数组,把原数组元素拷贝到新数组;
  • 核心特点:随机查询 O (1),尾部增删 O (1),中间增删 O (n)(需要移动元素),非线程安全。
个人理解

ArrayList 是开发中 90% 场景的首选,本质是对数组的封装,解决了数组长度固定的问题,核心优势是随机查询性能极高,适合读多写少、需要随机访问的场景;缺点是中间增删性能差,非线程安全。

项目实际使用场景
  1. 商品列表、订单列表、用户列表的查询返回:都是读多写少,需要随机分页查询,用 ArrayList 性能最高;
  2. 方法内的临时数据存储:比如批量导入商品时的临时数据存储,用 ArrayList;
  3. 避坑实践 :初始化 ArrayList 时如果知道数据量,指定初始容量,比如new ArrayList<>(1000),避免频繁扩容拷贝数组,提升性能。
面试考点标注

✅ 必问:ArrayList 的扩容机制(初始容量、扩容倍数、扩容流程);

✅ 必问:ArrayList 和数组的性能对比,什么时候用数组什么时候用 ArrayList;

✅ 坑点:ArrayList 的remove()方法,按索引删除和按对象删除的区别,遍历删除的并发修改异常。


2. LinkedList

核心定义
  • 底层结构 :双向链表,每个节点有previtemnext三个属性,没有初始容量,不需要扩容;
  • 核心特点:头尾增删 O (1),中间增删 O (n)(需要遍历找节点),随机查询 O (n),非线程安全;
  • 实现了Deque接口,可以作为队列、栈使用。
个人理解

LinkedList 的优势是头尾增删性能极高,适合频繁在头尾操作的场景;缺点是随机查询性能极差,因为要从头部 / 尾部遍历找节点,开发中绝大多数场景都不需要用 LinkedList,ArrayList 足够。

项目实际使用场景
  1. 订单操作日志、用户操作记录:频繁在尾部新增日志,不需要随机查询,用 LinkedList;
  2. 接口限流的请求队列:作为 FIFO 队列,频繁在尾部加请求,头部取请求,用 LinkedList;
  3. 避坑实践 :绝对不要用 LinkedList 做随机查询,比如for (int i=0; i<linkedList.size(); i++),性能是 ArrayList 的几十倍。
面试考点标注

✅ 必问:ArrayList 和 LinkedList 的核心区别、性能对比、适用场景;

✅ 细节:LinkedList 的底层是双向链表,不是单向链表,所以可以从头尾双向遍历。


3. Vector 与 CopyOnWriteArrayList(线程安全 List)

核心定义
(1)Vector
  • 底层和 ArrayList 一样是动态数组,所有方法都加了synchronized锁,锁整个数组,线程安全,性能极差,已经废弃,不推荐使用。
(2)CopyOnWriteArrayList(写时复制)
  • 核心原理:写操作(add/remove)时,复制一份新的数组,在新数组上修改,修改完成后把原数组的引用指向新数组;读操作直接读原数组,不需要加锁;
  • 核心特点:线程安全,读性能极高,写性能差(需要拷贝数组),适合读多写少的场景,是 fail-safe 机制(不会抛并发修改异常)。
个人理解

Vector 是粗暴的全方法加锁,性能太差被淘汰;CopyOnWriteArrayList 是读写分离的思想,读无锁,写加锁,适合读多写少的并发场景,缺点是有数据一致性问题(写操作完成前,读的是旧数据),只能保证最终一致性。

项目实际使用场景
  1. 商品分类全局缓存、字典缓存:读多写少,服务启动时加载一次,后续几乎不修改,多线程并发读,用 CopyOnWriteArrayList,不需要加锁,性能极高;
  2. 用户黑名单、IP 白名单:读多写少,偶尔新增,用 CopyOnWriteArrayList;
  3. 避坑实践:绝对不要在写多的场景用 CopyOnWriteArrayList,每次写都要拷贝数组,内存和性能开销极大。
面试考点标注

✅ 必问:fail-fast 和 fail-safe 的核心区别;

✅ 必问:CopyOnWriteArrayList 的实现原理、优缺点、适用场景;

✅ 坑点:CopyOnWriteArrayList 的内存拷贝开销和数据一致性问题。


模块三:Set 体系(无序、不可重复)

1. HashSet

核心定义
  • 底层结构 :基于 HashMap 实现,所有元素存在 HashMap 的 Key 中,Value 是一个固定的private static final Object PRESENT = new Object()常量;
  • 去重原理 :添加元素时,先计算元素的hashCode(),找到数组下标;如果下标位置没有元素,直接添加;如果有元素,再调用equals()对比内容,内容相同就不添加,不同就挂在链表上;
  • 核心特点:无序、去重、非线程安全,允许存 null。
个人理解

HashSet 本质是 HashMap 的马甲,所有逻辑都是 HashMap 实现的,核心价值就是利用 HashMap 的 Key 不可重复的特性实现去重,开发中用来做数据去重。

项目实际使用场景
  1. 用户浏览商品 ID 去重:用户多次浏览同一个商品,只保留一条记录,用 HashSet;
  2. 用户角色权限去重:一个用户多个角色,权限重复,用 HashSet 自动去重;
  3. 批量导入商品的重复校验:批量导入商品时,用 HashSet 校验商品编号是否重复。
面试考点标注

✅ 必问:HashSet 的去重原理;

✅ 坑点:自定义对象要实现hashCode()equals()才能正确去重,否则会出现重复元素。


2. LinkedHashSet

核心定义
  • 底层基于 LinkedHashMap 实现,继承 HashSet,在 HashMap 的基础上增加了双向链表,记录元素的插入顺序;
  • 核心特点:有序(按插入顺序)、去重、非线程安全,性能比 HashSet 略低。
项目实际使用场景
  1. 用户浏览历史:需要按用户浏览顺序保存,同时去重,用 LinkedHashSet;
  2. 商品搜索关键词历史:按搜索顺序保存,去重,用 LinkedHashSet。
面试考点标注

✅ 对比:HashSet 和 LinkedHashSet 的区别(无序 vs 有序)。


3. TreeSet

核心定义
  • 底层基于 TreeMap 实现,元素自动排序,支持自然排序和自定义排序;
  • 排序规则
    1. 自然排序:元素实现Comparable接口,重写compareTo()方法;
    2. 自定义排序:创建 TreeSet 时传入Comparator比较器;
  • 核心特点:有序(按排序规则)、去重、非线程安全,不允许存 null。
项目实际使用场景
  1. 商品按价格 / 销量排序的去重列表:需要排序 + 去重,用 TreeSet,传入 Comparator 按价格排序;
  2. 用户积分排行榜去重:按积分排序,去重,用 TreeSet。
面试考点标注

✅ 必问:Comparable 和 Comparator 的核心区别:

对比维度 Comparable Comparator
位置 元素类自身实现,在java.lang 单独的比较器类,在java.util
方法 重写compareTo(T o) 重写compare(T o1, T o2)
耦合 和元素类耦合,只能有一种排序规则 解耦,可以定义多种排序规则
场景 固定的自然排序 灵活的自定义排序

模块四:Map 体系(面试 100% 必问,重中之重)

1. HashMap(面试核心中的核心)

核心定义
(1)底层结构
  • JDK7:数组 + 单向链表,头插法,扩容时会出现死链问题;
  • JDK8 :数组 + 链表 + 红黑树,尾插法,解决死链问题,核心优化:
    • 链表长度≥8 且 数组长度≥64 时,链表转为红黑树,查询复杂度从 O (n) 降到 O (logn);
    • 红黑树节点数≤6 时,转回链表。
(2)核心参数
  • 默认初始容量:16,必须是 2 的 n 次方;
  • 默认负载因子:0.75;
  • 扩容阈值:容量 × 负载因子,达到阈值就扩容为原容量的 2 倍;
  • 红黑树转换阈值:链表长度 8,树转链表阈值 6。
(3)Hash 扰动函数

JDK8 的 hash 算法:(h = key.hashCode()) ^ (h >>> 16),把 hashCode 的高 16 位和低 16 位异或,让高位也参与下标计算,减少哈希冲突。

(4)扩容流程
  1. 数组容量达到阈值,创建新数组,容量为原数组的 2 倍;
  2. 遍历原数组的每个节点,重新计算节点在新数组的下标;
  3. JDK8 优化:不需要重新计算 hash,通过hash & oldCap判断,节点要么在原下标,要么在原下标 + 原容量的位置;
  4. 把节点迁移到新数组,完成扩容。

个人理解

HashMap 是哈希表的 Java 实现,核心是用数组做桶,链表 / 红黑树解决哈希冲突,JDK8 的红黑树优化解决了链表过长的查询性能问题,尾插法解决了 JDK7 扩容的死链问题;容量必须是 2 的 n 次方,是为了用hash & (length-1)代替取模运算,性能更高。


项目实际使用场景
  1. 商品 ID 到商品对象的本地缓存:Key 是商品 ID,Value 是 Goods 对象,O (1) 查询,性能极高;
  2. 接口参数封装、前端返回数据组装:用 HashMap 封装动态参数,灵活方便;
  3. 避坑实践
    • 初始化 HashMap 时指定初始容量,比如new HashMap<>(16),避免频繁扩容;
    • Key 推荐用 String、Integer 等不可变类,避免 Key 修改后 hashCode 变化,找不到数据;
    • 多线程环境下绝对不要用 HashMap,会出现数据丢失、死循环等问题。

面试考点标注

✅ 必问:JDK7 和 JDK8 HashMap 的核心区别:

对比维度 JDK7 HashMap JDK8 HashMap
底层结构 数组 + 单向链表 数组 + 链表 + 红黑树
插入方式 头插法 尾插法
扩容问题 多线程扩容会出现死链 尾插法解决死链问题
Hash 算法 4 次扰动 1 次扰动(高 16 位异或)
✅ 必问:HashMap 的完整扩容流程;
✅ 必问:HashMap 的容量为什么必须是 2 的 n 次方?(用位运算代替取模,性能更高,哈希分布更均匀);
✅ 必问:HashMap 的 Key 为什么推荐用不可变类?(hashCode 稳定,不会出现 Key 修改后找不到的问题);
✅ 细节:红黑树转换阈值为什么是 8?(泊松分布,链表长度到 8 的概率极低,平衡链表和红黑树的性能开销)。

2. ConcurrentHashMap(线程安全 Map,面试必问)

核心定义
(1)JDK7 实现:分段锁
  • 底层是 Segment 数组,每个 Segment 继承 ReentrantLock,是一个独立的小 HashMap;
  • 锁的粒度是 Segment,多个线程访问不同的 Segment 不会冲突,默认 16 个 Segment,并发度 16;
  • 缺点:锁粒度还是太大,同一个 Segment 的操作还是会竞争锁,性能不够高。
(2)JDK8 实现:CAS + synchronized
  • 底层结构和 JDK8 HashMap 一致:数组 + 链表 + 红黑树;
  • 锁的粒度是链表的头节点,只有哈希冲突的节点才会加锁,没有哈希冲突的节点用 CAS 无锁更新,并发度大幅提升;
  • 空节点用 CAS 插入,有节点的链表 / 红黑树用 synchronized 锁头节点;
  • 支持并发扩容,多线程一起迁移节点,提升扩容速度。

个人理解

ConcurrentHashMap 是 Java 中性能最高的线程安全 Map,JDK8 把锁粒度从 Segment 降到了单个节点,性能比 JDK7 提升了数倍,是多线程环境下的首选 Map;HashTable 是锁整个 Map,性能极差,已经完全被淘汰。


项目实际使用场景
  1. 多线程环境下的用户在线缓存、接口限流计数:多个线程同时读写,用 ConcurrentHashMap,线程安全,性能高;
  2. 商品库存的本地缓存:多线程同时扣减库存,用 ConcurrentHashMap 保证线程安全;
  3. 全局用户 Session 缓存:微服务网关多线程并发访问,用 ConcurrentHashMap。

面试考点标注

✅ 必问:JDK7 和 JDK8 ConcurrentHashMap 的线程安全实现区别;

✅ 必问:ConcurrentHashMap 和 HashTable 的区别;

✅ 原理:ConcurrentHashMap 为什么不用 ReentrantLock,改用 synchronized?(JDK6 之后 synchronized 做了大量优化,锁升级,性能不比 ReentrantLock 差,还节省内存);

✅ 细节:ConcurrentHashMap 的 Key 和 Value 为什么不能为 null?(避免多线程下的二义性,无法区分是 key 不存在还是 value 为 null)。


3. LinkedHashMap 与 TreeMap

核心定义
(1)LinkedHashMap
  • 继承 HashMap,底层 HashMap + 双向链表,记录元素的插入顺序 / 访问顺序;
  • 可以实现 LRU(最近最少使用)缓存,开启accessOrder=true,访问元素后会把元素移到链表尾部。
(2)TreeMap
  • 底层红黑树,Key 自动排序,支持自然排序和自定义排序,和 TreeSet 一致。
项目实际使用场景
  1. 购物车商品列表:按用户加入购物车的顺序保存,用 LinkedHashMap;
  2. 商品 LRU 本地缓存:缓存 1000 个热门商品,超过容量自动删除最久未访问的商品,用 LinkedHashMap 实现;
  3. 商品分类按 ID 排序:Key 是分类 ID,自动排序,用 TreeMap。
面试考点标注

✅ 场景题:怎么用 LinkedHashMap 实现 LRU 缓存;

  • LRU :最近最少使用,淘汰最久未访问的数据。

  • LinkedHashMap 核心特性

    • 继承 HashMap,底层哈希表 + 双向链表,有序

    • 构造方法 LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

      • accessOrder = true访问顺序(get/put 后元素移到链表尾部),LRU 核心依赖这个参数。

      • accessOrder = false:插入顺序(默认)。

  • 内置回调方法 removeEldestEntry(Map.Entry<K,V> eldest):返回 true 时,自动删除最久未使用的头部元素。

✅ 对比:HashMap、LinkedHashMap、TreeMap 的区别(无序、插入有序、排序有序)。


当日验收清单

  1. 不看资料口述:HashMap JDK7 和 JDK8 的核心差异、完整扩容流程;
  2. 不看资料口述:ConcurrentHashMap JDK8 的线程安全实现原理;
  3. 结合项目口述:你的项目中不同场景分别用了什么集合,为什么选这个;
  4. 避坑确认:遍历集合删除元素的并发修改异常怎么解决,多线程环境下用什么集合。

Java 集合框架 面试模拟题(实习面试专属,附标准答案)


一、基础必考题(8 道,中小厂实习 100% 覆盖)

1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?

【标准答案】

对比维度 ArrayList LinkedList
底层结构 动态数组 双向链表
随机查询 O (1),性能极高 O (n),需要遍历链表,性能极差
头尾增删 O(1) O(1)
中间增删 O (n),需要移动数组元素 O (n),需要遍历找节点
内存占用 连续内存,预留扩容空间 每个节点需要存 prev/next 指针,内存开销更大
适用场景 读多写少、需要随机访问的场景(90% 业务场景首选) 频繁头尾增删、不需要随机查询的场景(队列 / 栈)

【考点对应】模块二:List 体系核心对比


2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?

【标准答案】

对比维度 JDK7 HashMap JDK8 HashMap
底层结构 数组 + 单向链表 数组 + 链表 + 红黑树(链表长度≥8 且数组≥64 时树化)
插入方式 头插法 尾插法
扩容问题 多线程扩容会出现链表死循环(死链) 尾插法解决死链问题
Hash 算法 4 次扰动运算 1 次扰动(hashCode 高 16 位异或低 16 位)
扩容优化 每个节点重新计算 hash 下标 无需重新计算 hash,节点要么在原下标,要么在原下标 + 原容量

【考点对应】模块四:HashMap 核心特性


3. ConcurrentHashMap 和 HashTable 的核心区别是什么?

【标准答案】两者都是线程安全的 Map,核心差异在锁的实现和性能:

  1. 锁粒度不同
    • HashTable:锁整个 Map 对象,所有方法都加synchronized,所有操作都竞争同一把锁,性能极差;
    • ConcurrentHashMap:JDK7 是分段锁(锁 Segment),JDK8 是锁单个链表头节点,无冲突时用 CAS 无锁更新,锁粒度极小,并发性能极高。
  2. 底层结构不同:HashTable 是数组 + 链表;ConcurrentHashMap JDK8 是数组 + 链表 + 红黑树,和 HashMap 一致。
  3. null 值支持:HashTable 允许 Key/Value 为 null;ConcurrentHashMap 不允许 Key/Value 为 null(避免多线程下的二义性)。

【考点对应】模块四:ConcurrentHashMap 线程安全实现


4. HashSet 的去重原理是什么?

【标准答案】HashSet 底层基于 HashMap 实现,所有元素存在 HashMap 的 Key 中,Value 是一个固定的 Object 常量,去重逻辑和 HashMap 的 Key 去重完全一致:

  1. 添加元素时,先调用元素的hashCode()计算哈希值,找到数组对应的下标位置;
  2. 如果下标位置没有元素,直接添加成功;
  3. 如果下标位置有元素,再调用equals()对比两个元素的内容:
    • 内容相同:判定为重复元素,添加失败;
    • 内容不同:挂在链表 / 红黑树上,添加成功。

坑点:自定义对象必须重写hashCode()equals(),否则无法正确去重。

【考点对应】模块三:HashSet 去重原理


5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?

【标准答案】

对比维度 fail-fast fail-safe
代表集合 ArrayList、HashMap、HashSet 等普通集合 CopyOnWriteArrayList、ConcurrentHashMap 等并发集合
触发场景 遍历集合时,其他线程修改了集合结构(add/remove),立即抛出ConcurrentModificationException 遍历的是集合的快照,不会抛出异常
实现原理 基于modCount计数,遍历对比expectedModCountmodCount,不一致就抛异常 写时复制,遍历的是旧数组,修改操作在新数组上执行,互不影响
数据一致性 强一致性,遍历到的数据是最新的 最终一致性,遍历到的数据是旧快照,不保证实时一致

【考点对应】模块一:集合错误检测机制


6. ArrayList 的扩容机制是什么?

【标准答案】

  1. 初始容量:JDK8 及之后,默认初始容量为 0,第一次添加元素时才初始化为 10(懒加载,节省内存);
  2. 扩容触发:元素个数达到数组容量时,触发扩容;
  3. 扩容规则 :新容量 = 旧容量的 1.5 倍(位运算oldCapacity + (oldCapacity >> 1));
  4. 扩容流程:创建新容量的数组,将原数组的元素拷贝到新数组,更新数组引用。

优化技巧:初始化 ArrayList 时如果知道数据量,指定初始容量,避免频繁扩容拷贝数组。

【考点对应】模块二:ArrayList 核心原理


7. 简述 HashMap 的 put () 方法完整执行流程(JDK8)

【标准答案】

  1. 计算 Key 的 hash 值:hash = (h = key.hashCode()) ^ (h >>> 16)
  2. 判断数组是否为空,为空则初始化数组(默认容量 16,负载因子 0.75);
  3. 计算数组下标:i = (n - 1) & hash
  4. 如果下标位置为空,用 CAS 直接插入新节点,结束;
  5. 如果下标位置有节点:
    • 头节点的 Key 和当前 Key 相同,直接覆盖 Value;
    • 节点是红黑树节点,调用红黑树的插入方法;
    • 节点是链表节点,遍历链表,找到相同 Key 则覆盖,找不到则在链表尾部插入新节点;插入后判断链表长度是否≥8,是则触发树化;
  6. 插入完成后,判断元素个数是否达到扩容阈值,达到则触发扩容。

【考点对应】模块四:HashMap 核心流程


8. Comparable 和 Comparator 的核心区别是什么?

【标准答案】

对比维度 Comparable Comparator
定义位置 元素类自身实现,属于java.lang 独立的比较器类,属于java.util
核心方法 重写int compareTo(T o) 重写int compare(T o1, T o2)
耦合性 和元素类强耦合,一个类只能有一种排序规则 和元素类解耦,可以定义多种排序规则
适用场景 元素的固定自然排序(比如 ID、价格排序) 灵活的自定义排序(比如按销量、浏览量动态排序)

【考点对应】模块三:TreeSet/TreeMap 排序规则


二、场景坑点题(6 道,大厂实习高频代码题 / 业务题)

1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?

java 复制代码
List<Goods> goodsList = new ArrayList<>();
goodsList.add(new Goods(1, "手机", true));
goodsList.add(new Goods(2, "电脑", false));
goodsList.add(new Goods(3, "平板", false));

// 遍历删除下架商品
for (Goods goods : goodsList) {
    if (!goods.isOnSale()) {
        goodsList.remove(goods);
    }
}

【标准答案】报错:ConcurrentModificationException(并发修改异常)原因:foreach 循环底层是 Iterator 迭代器,直接调用list.remove()会修改集合的modCount计数,迭代器检测到计数和预期不一致,触发 fail-fast 机制抛出异常。正确写法:使用 Iterator 的 remove () 方

java 复制代码
Iterator<Goods> iterator = goodsList.iterator();
while (iterator.hasNext()) {
    Goods goods = iterator.next();
    if (!goods.isOnSale()) {
        iterator.remove();
    }
}

【考点对应】模块一:fail-fast 机制、集合遍历删除坑点


2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?

java 复制代码
public class Goods {
    private Long id;
    private String title;
    // 构造方法、getter/setter,未重写hashCode和equals
}

public class Test {
    public static void main(String[] args) {
        Set<Goods> goodsSet = new HashSet<>();
        goodsSet.add(new Goods(1L, "手机"));
        goodsSet.add(new Goods(1L, "手机"));
        System.out.println(goodsSet.size());
    }
}

【标准答案】输出结果:2原因:HashSet 去重依赖hashCode()equals()方法,Goods 类没有重写这两个方法,默认使用 Object 类的实现:

  • hashCode()默认返回对象的内存地址,两个 new 出来的对象地址不同,hash 值不同;
  • equals()默认比较对象地址,两个对象地址不同,判定为不重复,因此添加了两个元素。修复:重写 Goods 类的hashCode()equals(),按 id 判断是否为同一个商品。

【考点对应】模块三:HashSet 去重坑点


3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?

【标准答案】HashMap 是非线程安全的,多线程环境下会出现 3 个核心问题:

  1. 数据丢失:多个线程同时 put 同一个下标位置,会出现节点覆盖,导致数据丢失;
  2. 死循环(JDK7):多线程扩容时,头插法会导致链表形成环,get 元素时死循环,CPU 占满 100%;
  3. 数据不一致 :一个线程 put 元素,另一个线程 get 不到最新数据。替代方案:多线程环境下必须用ConcurrentHashMap,线程安全且性能极高。

【考点对应】模块四:HashMap 线程安全问题、ConcurrentHashMap 适用场景


4. 以下场景分别应该用什么 List 集合?为什么?

场景 1:商品分类全局缓存,服务启动时加载一次,后续几乎不修改,多线程高并发读;

场景 2:订单操作日志,频繁在尾部新增,不需要随机查询;

场景 3:商品列表分页查询,读多写少,需要随机访问。

【标准答案】

  1. 场景 1:用CopyOnWriteArrayList原因:读多写少,CopyOnWriteArrayList 读无锁,性能极高,适合并发读的缓存场景;
  2. 场景 2:用LinkedList原因:频繁尾部增删,不需要随机查询,LinkedList 尾部增删 O (1),性能更高;
  3. 场景 3:用ArrayList原因:读多写少,需要随机分页查询,ArrayList 随机访问 O (1),性能最高。

【考点对应】模块二:List 集合选型、CopyOnWriteArrayList 适用场景


5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?

java 复制代码
// 要存10个商品,初始化容量为10
Map<Long, Goods> goodsMap = new HashMap<>(10);

【标准答案】问题:HashMap 的扩容阈值 = 容量 × 负载因子(默认 0.75),容量 10 的阈值是 7,存入第 8 个元素就会触发扩容,造成不必要的性能开销。正确写法:初始容量 = 预期元素数 / 负载因子 + 1,存 10 个元素的话,初始容量设为 16(10 / 0.75 + 1 ≈ 14,向上取 2 的 n 次方为 16)。

【考点对应】模块四:HashMap 初始容量优化


6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。

【标准答案】LinkedHashMap 天然支持 LRU,核心实现:

  1. 开启accessOrder = true,访问元素后会把元素移到链表尾部;
  2. 重写removeEldestEntry()方法,当元素个数超过容量时,删除链表头部的最久未访问元素。代码实现:
java 复制代码
public class GoodsLruCache extends LinkedHashMap<Long, Goods> {
    private static final int MAX_CAPACITY = 100;

    // 第三个参数accessOrder设为true,开启访问顺序
    public GoodsLruCache() {
        super(MAX_CAPACITY, 0.75f, true);
    }

    // 重写删除策略,超过容量删除最久未访问的元素
    @Override
    protected boolean removeEldestEntry(Map.Entry<Long, Goods> eldest) {
        return size() > MAX_CAPACITY;
    }
}

【考点对应】模块四:LinkedHashMap 特性、LRU 缓存实现


三、进阶原理题(4 道,中大厂实习拔高题)

1. HashMap 的容量为什么必须是 2 的 n 次方?

【标准答案】核心原因有两个:

  1. 性能优化 :计算数组下标的时候,用(n - 1) & hash位运算代替hash % n取模运算,位运算的性能远高于取模运算,只有当 n 是 2 的 n 次方时,(n-1) & hash才等价于取模;
  2. 哈希分布均匀:2 的 n 次方时,n-1 的二进制全是 1,哈希值的每一位都能参与下标计算,减少哈希冲突,让元素均匀分布在数组中。

【考点对应】模块四:HashMap 底层设计原理


2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?

【标准答案】核心原因有三个:

  1. 性能优化:JDK6 之后 synchronized 做了大量优化(偏向锁、轻量级锁、锁升级),无竞争场景下性能比 ReentrantLock 更高;
  2. 锁粒度更细:JDK8 锁的是单个链表头节点,锁竞争的概率极低,不需要 ReentrantLock 的 Condition、中断等高级功能,synchronized 足够;
  3. 节省内存:ReentrantLock 需要基于 AQS 实现,每个锁对象都要维护大量额外信息,内存开销远大于 synchronized。

【考点对应】模块四:ConcurrentHashMap 锁优化


3. HashMap 的链表转红黑树的阈值为什么是 8?

【标准答案】基于泊松分布的概率统计:

  1. 理想哈希算法下,链表长度达到 8 的概率是0.00000006(千万分之六),几乎不可能出现,用 8 作为阈值可以避免频繁的链表和红黑树转换,平衡性能开销;
  2. 红黑树的查询性能是 O (logn),链表是 O (n),链表长度小于 8 时,遍历性能和红黑树差距不大,不需要树化;
  3. 树转链表的阈值是 6,留 2 个差值,避免频繁的树化和退化。

【考点对应】模块四:HashMap 红黑树设计原理


4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?

【标准答案】核心原因是 JDK7 用头插法 + 单链表,多线程扩容时会导致链表形成环:

  1. 线程 1 和线程 2 同时触发扩容,都拿到了原链表的头节点;
  2. 线程 1 先完成扩容,用头插法把链表顺序反转了;
  3. 线程 2 继续执行扩容,按照原顺序遍历节点,头插法会把节点的 next 指针指向已经反转的节点,最终形成环形链表;
  4. 后续 get 元素遍历环形链表时,会无限循环,CPU 占满 100%。JDK8 用尾插法,保持链表的原有顺序,解决了死循环问题。

【考点对应】模块四:HashMap JDK7 死链问题


实习面试答题加分技巧(绑定你的校园二手平台项目)

  1. 所有集合题都绑定项目选型
    • 问 ConcurrentHashMap:主动说「我做校园二手平台的接口限流、用户在线状态缓存,就是用的 ConcurrentHashMap,多线程并发读写,线程安全性能高,之前踩过 HashMap 多线程数据丢失的坑」;
    • 问 ArrayList 优化:主动说「我做商品列表批量查询的时候,会根据分页大小指定 ArrayList 的初始容量,避免频繁扩容,性能提升很明显」;
  2. 主动说踩坑经验:比如问遍历删除,就说「我之前做商品下架功能的时候,用 foreach 删除元素踩过并发修改异常的坑,后来改成 Iterator 的 remove () 方法解决了」;
  3. 答题逻辑固定为「结论→原理→我的项目实践」,比单纯背知识点的候选人认可度高很多。
相关推荐
NE_STOP9 小时前
Vide Coding--AI编程工具的选择
java
LDR0069 小时前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术9 小时前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园9 小时前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆9 小时前
JDK 下载安装与配置详细教程
java·前端·ai编程
小宇宙Zz9 小时前
Maven依赖冲突
java·服务器·maven
swordbob9 小时前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
咖啡八杯10 小时前
GoF设计模式——享元模式
java·spring·设计模式·享元模式
十五喵源码网10 小时前
基于springboot2+vue2的租房管理系统
java·毕业设计·springboot·论文笔记
摇滚侠10 小时前
IDEA 创建 Java 项目 手动整合 SSM 框架
java·ide·intellij-idea