2026java面试题集合篇
负载因子 = HashMap 实际元素个数(size) / 数组长度(capacityCopyOnWriteArrayList 是 Java 提供的线程安全 List 实现,核心机制是“写时复制(Copy-On-Write)”,即读操作无锁,写操作(addremove)时复制一份新数组,修改后替换原数组,保证读操作的线程安全。核心结论:fail-safe(安全失败)是集合迭代器的特性,迭代时遍历集
Java 集合面试题详细解答
以下按题目顺序逐一解答,每个问题包含核心概念、举例说明和关键要点,兼顾理解与记忆。
1、说说常见的集合有哪些?
核心概念
Java 集合框架核心分为 Collection(存储单个元素)和 Map(存储键值对)两大体系,常见集合按功能分类如下:
详细分类+举例
一、Collection 接口(单元素存储)
-
List(有序、可重复)
ArrayList:基于动态数组,查询快、增删慢(如new ArrayList<>()存储用户列表);LinkedList:基于双向链表,增删快、查询慢(如new LinkedList<>()实现队列);Vector:线程安全的动态数组(古老类,性能差,如new Vector<>()兼容旧系统);CopyOnWriteArrayList:写时复制的线程安全 List(如高并发读场景存储配置项)。
-
Set(无序、不可重复)
HashSet:基于 HashMap,查询快(如new HashSet<>()存储用户 ID 去重);LinkedHashSet:基于 LinkedHashMap,有序(插入顺序)(如new LinkedHashSet<>()记录访问历史);TreeSet:基于 TreeMap,自然排序(如new TreeSet<>()对成绩排序)。
-
Queue(队列,FIFO)
ArrayDeque:基于数组的双端队列(如new ArrayDeque<>()实现栈/队列);LinkedList:可作为双端队列使用;PriorityQueue:优先级队列(如new PriorityQueue<>()按优先级处理任务);- 阻塞队列:
ArrayBlockingQueue( bounded 队列)、LinkedBlockingQueue(多线程通信)。
二、Map 接口(键值对存储,key 唯一)
HashMap:无序、查询快(如new HashMap<>()存储用户信息(ID-User));LinkedHashMap:有序(插入/访问顺序)(如new LinkedHashMap<>()实现 LRU 缓存);TreeMap:自然排序(如new TreeMap<>()按日期排序存储日志);Hashtable:线程安全(古老类,如new Hashtable<>()兼容旧系统);ConcurrentHashMap:高效线程安全(如new ConcurrentHashMap<>()高并发缓存);WeakHashMap:弱引用 Map(key 被 GC 后自动删除,如new WeakHashMap<>()临时缓存)。
关键要点
- 优先使用 JDK 8+ 集合(如
HashMap替代Hashtable); - 线程安全场景选
ConcurrentHashMap/CopyOnWriteArrayList,而非Vector/Hashtable。
2、哪些集合类可对元素的随机访问?
核心概念
随机访问指通过索引快速获取元素(时间复杂度 O(1)),需实现 RandomAccess 接口(标记接口,无抽象方法)。
支持随机访问的集合+举例
ArrayList:基于动态数组,索引直接映射数组下标(如list.get(3)快速获取第 4 个元素);Vector:动态数组实现,支持vector.get(2)随机访问;CopyOnWriteArrayList:数组底层,支持cowList.get(5)随机访问;ArrayDeque:数组实现的双端队列,支持deque.get(1)索引访问;- 数组(
T[]):本质是集合底层实现,天然支持随机访问(如arr[2])。
不支持随机访问的集合
LinkedList:链表结构,需遍历查找(list.get(1000)需遍历 1000 个节点);HashSet/TreeSet:无索引概念,需迭代器访问;HashMap/TreeMap:通过 key 访问,无索引随机访问。
关键要点
- 用
instanceof RandomAccess判断是否支持随机访问(如list instanceof RandomAccess); - 随机访问场景优先选
ArrayList,避免LinkedList。
3、Comparable 和 Comparator 接口的区别?
核心概念
两者均用于对象排序,核心区别在于排序逻辑的定义位置(内部 vs 外部)。
详细对比+举例
| 特性 | Comparable(自然排序) | Comparator(定制排序) |
|---|---|---|
| 定义位置 | 排序逻辑在被排序类内部(实现接口) | 排序逻辑在外部类(单独实现) |
| 核心方法 | int compareTo(T o)(自身与参数比较) |
int compare(T o1, T o2)(两个参数比较) |
| 灵活性 | 固定排序(不可动态修改) | 灵活(可定义多个比较器) |
| 举例 | String 实现 Comparable,默认字典序排序 |
自定义 Comparator 实现倒序排序 |
举例说明
- Comparable 示例(自然排序)
// 实体类实现 Comparable,定义自然排序(按年龄升序)
class Person implements Comparable<Person> {
private int age;
@Override
public int compareTo(Person o) {
return this.age - o.age; // 自身年龄 - 参数年龄 = 升序
}
}
// 使用:Collections.sort(personList) 自动按年龄排序
- Comparator 示例(定制排序)
// 外部定义比较器,实现倒序排序(不修改 Person 类)
Comparator<Person> ageDescComparator = (p1, p2) -> p2.age - p1.age;
// 使用:Collections.sort(personList, ageDescComparator) 按年龄倒序
关键要点
- 需固定排序(如 String、Integer)用
Comparable; - 需动态切换排序规则(如升序/倒序)用
Comparator; TreeSet/TreeMap默认使用Comparable,也可传入Comparator。
4、Collection 和 Collections 的区别?
核心概念
Collection 是集合顶层接口,Collections 是集合工具类,两者本质完全不同。
详细对比+举例
| 特性 | Collection | Collections |
|---|---|---|
| 类型 | 接口(Interface) | 工具类(Class) |
| 作用 | 定义集合通用操作规范(add、remove、iterator) | 提供静态工具方法(排序、查找、同步化) |
| 实例化 | 不能直接实例化,需通过实现类(如 ArrayList) | 不能实例化(构造器私有),仅用静态方法 |
| 举例 | Collection<String> list = new ArrayList<>() |
Collections.sort(list)、Collections.synchronizedList(list) |
举例说明
// Collection:接口使用(通过 ArrayList 实现)
Collection<String> coll = new ArrayList<>();
coll.add("a");
coll.remove("a");
// Collections:工具类使用(静态方法)
List<Integer> list = Arrays.asList(3,1,2);
Collections.sort(list); // 排序:[1,2,3]
Collections.reverse(list); // 反转:[3,2,1]
List<Integer> syncList = Collections.synchronizedList(list); // 转为线程安全集合
关键要点
Collection是集合的“骨架”,定义行为;Collections是“工具包”,简化集合操作;- 常用方法:
Collections.sort()(排序)、Collections.binarySearch()(二分查找)、Collections.emptyList()(空集合)。
5、Enumeration 和 Iterator 接口的区别?
核心概念
两者均用于遍历集合,Enumeration 是 JDK 1.0 古老接口,Iterator 是其替代者(JDK 1.2+),功能更完善。
详细对比+举例
| 特性 | Enumeration | Iterator |
|---|---|---|
| 诞生版本 | JDK 1.0 | JDK 1.2 |
| 核心方法 | hasMoreElements()、nextElement() |
hasNext()、next()、remove() |
| 功能 | 仅支持遍历(读) | 支持遍历+删除(读写) |
| 安全性 | 遍历中修改集合无异常(fail-safe) | 遍历中修改集合抛 ConcurrentModificationException(fail-fast) |
| 适用集合 | 古老集合(Vector、Hashtable) | 所有 Collection 实现类(List、Set、Queue) |
| 举例 | Vector.elements() |
List.iterator() |
举例说明
- Enumeration 遍历 Vector
Vector<String> vector = new Vector<>();
vector.add("a");
Enumeration<String> en = vector.elements();
while (en.hasMoreElements()) {
System.out.println(en.nextElement()); // 仅遍历,无法删除
}
- Iterator 遍历 ArrayList
List<String> list = new ArrayList<>();
list.add("a");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("a".equals(s)) {
it.remove(); // 支持删除,无异常
}
}
关键要点
- 优先使用
Iterator(功能全、安全); Enumeration仅用于兼容旧代码(如维护 Vector/Hashtable 遍历);Iterator的remove()是唯一安全的集合遍历删除方式。
6、集合使用泛型有什么优点?
核心概念
泛型(Generic)为集合指定存储元素的类型,核心优点是类型安全、代码简洁、避免类型转换异常。
优点+举例
-
编译期类型安全:限制集合存储类型,禁止存入错误元素(编译报错)
List<String> list = new ArrayList<>(); list.add("a"); // list.add(123); // 编译报错:类型不兼容,避免存入 Integer -
消除强制类型转换:遍历无需手动转换,代码简洁
// 无泛型:需强制转换 List rawList = new ArrayList(); rawList.add("a"); String s1 = (String) rawList.get(0); // 强制转换,可能抛 ClassCastException // 有泛型:无需转换 List<String> genList = new ArrayList<>(); genList.add("a"); String s2 = genList.get(0); // 直接获取 String 类型 -
代码复用:一套泛型代码适配多种类型
// 泛型方法:适配 List<String>、List<Integer> 等 public static <T> void printList(List<T> list) { for (T t : list) { System.out.println(t); } } // 使用:打印 String 列表和 Integer 列表 printList(Arrays.asList("a", "b")); printList(Arrays.asList(1, 2)); -
可读性提升:明确集合存储类型,语义清晰
Map<String, User> userMap = new HashMap<>(); // 明确 key 是 String,value 是 User
关键要点
- 泛型仅在编译期生效(类型擦除),运行时无泛型信息;
- 集合泛型不可使用基本类型(如
List<int>错误),需用包装类(List<Integer>); - 泛型提升代码安全性和可读性,是集合的必用特性。
7、List、Set、Map 之间的区别是什么?
核心概念
三者是集合框架的核心组件,核心区别体现在存储结构、元素特性和使用场景。
详细对比+举例
| 特性 | List | Set | Map |
|---|---|---|---|
| 存储结构 | 单元素,有序(插入顺序),可重复 | 单元素,无序(TreeSet 除外),不可重复 | 键值对(key-value),key 唯一,value 可重复 |
| 核心方法 | add(E e)、get(int index)、set(int index, E e) |
add(E e)、contains(Object o)、remove(Object o) |
put(K key, V value)、get(Object key)、containsKey(Object key) |
| 元素访问 | 支持索引访问(get(index)) |
无索引,仅迭代器访问 | 无索引,通过 key 访问 value |
| 有序性 | 有序(插入顺序不变) | 无序(HashSet)/有序(TreeSet 自然排序、LinkedHashSet 插入顺序) | 无序(HashMap)/有序(TreeMap 自然排序、LinkedHashMap 插入/访问顺序) |
| 重复性 | 允许重复元素 | 不允许重复元素(依赖 equals() 和 hashCode()) |
key 不允许重复(覆盖),value 允许重复 |
| 底层实现 | ArrayList(数组)、LinkedList(链表) | HashSet(哈希表)、TreeSet(红黑树) | HashMap(哈希表)、TreeMap(红黑树) |
| 适用场景 | 按顺序存储、允许重复、频繁访问(如购物车、列表展示) | 去重、无需顺序(如用户 ID 集合、标签集合) | 键值映射(如缓存、字典、用户 ID-信息映射) |
举例说明
// List:有序可重复
List<String> list = new ArrayList<>();
list.add("a");
list.add("a"); // 允许重复
System.out.println(list); // [a, a](有序)
// Set:无序不可重复
Set<String> set = new HashSet<>();
set.add("a");
set.add("a"); // 自动去重
System.out.println(set); // [a](无序)
// Map:键值对,key 唯一
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("a", 2); // key 重复,覆盖 value
System.out.println(map); // {a=2}(无序)
关键要点
- 需顺序+重复 → List;
- 需去重+无序 → Set;
- 需键值映射 → Map;
- 有序需求优先选
LinkedHashSet/LinkedHashMap,排序需求选TreeSet/TreeMap。
8、为什么 Map 接口不继承 Collection 接口?
核心概念
Map 不继承 Collection 的核心原因是 存储模型完全不同:Collection 是“单元素集合”,Map 是“键值对映射”,强行继承会破坏接口语义一致性。
详细原因+举例
-
存储结构冲突
- Collection 定义“单个元素”的操作规范(如
add(E e)添加单个元素); - Map 定义“键值对”的操作规范(如
put(K key, V value)添加键值对); - 若 Map 继承 Collection,需实现
add(E e),但 Map 存储的是键值对,无法适配“添加单个元素”的语义(如map.add("a")无法确定是 key 还是 value)。
- Collection 定义“单个元素”的操作规范(如
-
遍历方式冲突
- Collection 通过
Iterator遍历单个元素; - Map 需遍历 key(
keySet())、value(values())或键值对(entrySet()),遍历逻辑与 Collection 完全不同。
- Collection 通过
-
接口设计原则
Java 集合框架遵循“单一职责原则”:Collection 专注于“单元素集合”,Map 专注于“键值对映射”,两者各司其职,通过工具方法(如Collections)关联,而非继承。
关联方式举例
Map 虽不继承 Collection,但可通过以下方法转换为 Collection 视图:
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
// 转换为 Collection 视图
Set<String> keys = map.keySet(); // key 的 Set 集合(继承 Collection)
Collection<Integer> values = map.values(); // value 的 Collection 集合
Set<Map.Entry<String, Integer>> entries = map.entrySet(); // 键值对的 Set 集合
关键要点
- 核心矛盾:存储模型(单元素 vs 键值对)不同;
- 设计原则:单一职责,避免接口语义混乱;
- 关联方式:通过
keySet()/values()/entrySet()与 Collection 关联。
9、常用的线程安全的 Map 有哪些?
核心概念
线程安全的 Map 指多线程并发访问时,无需额外同步即可保证数据一致性,常用的有 Hashtable、ConcurrentHashMap、Collections.synchronizedMap()。
详细对比+举例
| 特性 | Hashtable | ConcurrentHashMap | Collections.synchronizedMap(Map) |
|---|---|---|---|
| 实现方式 | 所有方法加 synchronized(对象级锁) |
JDK 7:分段锁(Segment);JDK 8+:CAS + synchronized(哈希桶级锁) | 包装普通 Map,所有方法通过 synchronized 同步(对象级锁) |
| 性能 | 差(同一时间仅一个线程访问) | 优(锁粒度小,支持高并发) | 一般(锁粒度大,与 Hashtable 类似) |
| key/value 为 null | 不允许(抛 NPE) | 不允许(抛 NPE) | 允许(取决于底层 Map,如 HashMap 支持) |
| 原子操作 | 无 | 支持(如 putIfAbsent(K key, V value)) |
无 |
| 适用场景 | 旧代码维护、低并发场景 | 高并发场景(推荐) | 低并发场景、需包装自定义 Map |
| 举例 | new Hashtable<>() |
new ConcurrentHashMap<>() |
Collections.synchronizedMap(new HashMap<>()) |
举例说明
- ConcurrentHashMap(高并发推荐)
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("a", 1);
chm.putIfAbsent("b", 2); // 原子操作:不存在则插入
System.out.println(chm.get("b")); // 2
- Collections.synchronizedMap(低并发)
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> syncMap = Collections.synchronizedMap(hashMap);
syncMap.put("a", 1);
System.out.println(syncMap.get("a")); // 1
- Hashtable(旧代码)
Hashtable<String, Integer> ht = new Hashtable<>();
ht.put("a", 1);
// ht.put(null, 2); // 抛 NullPointerException
关键要点
- 高并发场景优先选
ConcurrentHashMap(性能最优); - 低并发+需灵活包装 →
Collections.synchronizedMap; - 旧代码维护 →
Hashtable(不推荐新代码); - 避免使用
HashMap手动加锁(易死锁,性能差)。
10、HashMap 与 Hashtable 的区别?
核心概念
两者均基于哈希表实现 Map,但 Hashtable 是古老类,HashMap 是其优化替代者,核心区别体现在线程安全、性能、功能支持等方面。
详细对比+举例
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 线程不安全(多线程需手动同步) | 线程安全(所有方法加 synchronized) |
| 性能 | 高(无锁开销,JDK 8+ 红黑树优化) | 低(对象级锁,并发阻塞) |
| key/value 为 null | 允许 key 为 null(仅一个),value 可为 null | 不允许 key/value 为 null(抛 NPE) |
| 底层实现 | JDK 8+:数组 + 链表/红黑树(链表长度>8 转红黑树) | 数组 + 链表(无红黑树优化) |
| 初始容量/扩容机制 | 初始容量 16,扩容因子 0.75,扩容为原容量 2 倍 | 初始容量 11,扩容因子 0.75,扩容为原容量 2 倍 + 1 |
| 迭代器类型 | 快速失败(fail-fast):遍历中修改抛 ConcurrentModificationException |
安全失败(fail-safe):遍历中修改不抛异常(枚举器遍历副本) |
| 诞生版本 | JDK 1.2 | JDK 1.0 |
| 适用场景 | 单线程/低并发场景(推荐) | 旧代码维护、需线程安全的低并发场景 |
| 举例 | new HashMap<>() 存储用户配置 |
new Hashtable<>() 兼容 legacy 系统 |
举例说明
// HashMap:支持 key 为 null
HashMap<String, Integer> map = new HashMap<>();
map.put(null, 1);
System.out.println(map.get(null)); // 1
// Hashtable:不支持 key 为 null
Hashtable<String, Integer> ht = new Hashtable<>();
// ht.put(null, 1); // 抛 NullPointerException
关键要点
- 新代码优先选
HashMap(性能优、功能全); - 线程安全需求选
ConcurrentHashMap,而非Hashtable; - 避免在 HashMap 中大量存储 null key/value(易混淆)。
11、HashMap 和 TreeMap 怎么选?
核心概念
两者选型核心取决于 是否需要有序、查询/插入性能需求:HashMap 无序但查询快,TreeMap 有序但查询/插入性能略低。
详细对比+选型建议
| 特性 | HashMap | TreeMap |
|---|---|---|
| 有序性 | 无序(key 按哈希值存储) | 有序(自然排序/自定义排序) |
| 底层实现 | 数组 + 链表/红黑树(JDK 8+) | 红黑树(平衡二叉搜索树) |
| 查询性能 | 优秀(平均 O(1),最坏 O(logn)) | 良好(O(logn)) |
| 插入/删除性能 | 优秀(平均 O(1)) | 良好(O(logn),红黑树旋转维护平衡) |
| key 要求 | 无强制要求(需重写 equals() 和 hashCode() 保证去重) |
key 需实现 Comparable 或传入 Comparator(否则抛 ClassCastException) |
| key/value 为 null | 允许 key 为 null(仅一个),value 可为 null | 不允许 key 为 null,value 可为 null |
| 适用场景 | 无需有序、追求高并发查询/插入(如缓存、字典) | 需要按 key 排序(如按时间戳排序日志、范围查询) |
举例说明
- 选 HashMap(无需有序)
// 缓存用户信息(ID-User),无需排序,追求查询快
Map<String, User> userCache = new HashMap<>();
userCache.put("1001", new User("张三"));
User user = userCache.get("1001"); // O(1) 快速查询
- 选 TreeMap(需要有序)
// 按日期排序存储日志(日期-日志内容)
Map<LocalDate, String> logMap = new TreeMap<>();
logMap.put(LocalDate.of(2024, 5, 20), "日志1");
logMap.put(LocalDate.of(2024, 5, 19), "日志2");
// 遍历自动按日期升序
for (Map.Entry<LocalDate, String> entry : logMap.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// 输出:2024-05-19:日志2;2024-05-20:日志1
关键要点
- 无需有序 → HashMap(性能优先);
- 需要排序/范围查询 → TreeMap(功能优先);
- 需插入顺序 → 选
LinkedHashMap(性能优于 TreeMap); - TreeMap 的 key 必须支持比较(实现
Comparable或传入Comparator)。
12、HashMap 的数据结构是什么?
核心概念
HashMap 的底层数据结构随 JDK 版本演进,核心是“哈希表”,JDK 8+ 引入红黑树优化长链表查询性能。
详细结构+举例
一、JDK 7 及之前:数组 + 链表(头插法)
- 数组(哈希桶):存储
Entry<K, V>节点,数组长度为 2 的幂(默认 16); - 链表:解决哈希冲突(不同 key 哈希后映射到同一数组下标),冲突节点以链表形式存储(头插法);
- 问题:链表过长时,查询时间复杂度从 O(1) 退化到 O(n)。
二、JDK 8 及之后:数组 + 链表/红黑树(尾插法)
- 数组(哈希桶):存储
Node<K, V>节点(替代 JDK 7 的 Entry),数组长度仍为 2 的幂; - 链表:哈希冲突时,节点以链表形式存储(尾插法,避免多线程扩容死循环);
- 红黑树:当链表长度超过阈值(默认 8),且数组长度 >= 64 时,链表转为红黑树;当链表长度小于阈值(默认 6)时,红黑树转回链表;
- 优化目的:红黑树查询时间复杂度为 O(logn),解决长链表查询慢问题。
结构示意图
数组(长度16)
|
0 → null
1 → Node1 → Node2 → Node3 (链表,长度<8)
2 → null
...
5 → NodeA → NodeB → ... → NodeI (链表长度>8,转为红黑树)
举例说明(JDK 8 结构)
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
// 若 "a"、"b" 哈希冲突,存储在同一数组下标,形成链表;
// 若后续添加更多冲突节点,链表长度>8 且数组长度>=64,转为红黑树
关键要点
- JDK 8 核心优化:红黑树替代长链表,尾插法替代头插法;
- 数组长度始终为 2 的幂(保证哈希分布均匀);
- 红黑树转换条件:链表长度>8 + 数组长度>=64(避免数组过小时转换)。
13、HashMap 在 JDK 8 中有哪些改变?
核心概念
JDK 8 对 HashMap 进行了大幅优化,核心改变围绕“性能提升”和“稳定性增强”,主要包括 5 点。
详细改变+举例
-
底层数据结构优化:数组 + 链表/红黑树
- JDK 7:数组 + 链表(头插法);
- JDK 8:数组 + 链表(尾插法)/ 红黑树(链表长度>8 且数组长度>=64 时转换);
- 优化效果:查询时间复杂度从 O(n) 优化到 O(logn)。
-
链表插入方式:尾插法替代头插法
- JDK 7:头插法(新节点插入链表头部),多线程扩容时可能导致链表环化(死循环);
- JDK 8:尾插法(新节点插入链表尾部),避免多线程扩容时的死循环问题(虽 HashMap 仍线程不安全,但稳定性提升)。
-
哈希值计算优化
- JDK 7:
hash(key)方法通过多次移位和异或计算哈希值; - JDK 8:
hash(key)方法简化为key.hashCode() ^ (key.hashCode() >>> 16)(高位异或低位),减少哈希冲突,让哈希值分布更均匀。
- JDK 7:
-
扩容机制优化
- JDK 7:扩容时需重新计算每个节点的哈希值和数组下标;
- JDK 8:扩容时通过
(原数组长度 & 原哈希值)判断节点是否需要移动(0 则留在原位置,非 0 则移动到新数组原下标 + 原数组长度位置),避免重新计算哈希值,提升扩容效率。
-
节点类优化
- JDK 7:
Entry<K, V>类(存储 key、value、hash、next 指针); - JDK 8:
Node<K, V>类(替代 Entry),红黑树节点为TreeNode<K, V>(继承 Node,增加红黑树相关属性:parent、left、right、red 等)。
- JDK 7:
举例说明(哈希值计算优化)
// JDK 8 hash 方法简化
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 高位异或低位,让哈希值分布更均匀,减少冲突
关键要点
- 核心优化:红黑树、尾插法、扩容效率提升;
- 目的:解决 JDK 7 长链表查询慢、多线程扩容死循环问题;
- 性能提升:查询、扩容性能大幅优化,尤其适合大数据量场景。
14、HashMap 的 put 方法逻辑?
核心概念
HashMap 的 put(K key, V value) 方法核心是“计算哈希值 → 定位数组下标 → 处理哈希冲突 → 插入节点 → 扩容判断”,JDK 8 后的完整逻辑如下。
详细逻辑+举例
1. 计算 key 的哈希值和数组下标
- 若 key 为 null:哈希值为 0,下标为 0;
- 若 key 不为 null:通过
hash(key)方法计算哈希值(高位异或低位); - 数组下标 =
(数组长度 - 1) & 哈希值(保证下标在数组范围内)。
2. 定位哈希桶,处理插入逻辑
获取数组下标对应的节点(table[index]),分三种情况:
- 情况 1:哈希桶为空(table[index] == null):直接创建新 Node 节点,放入哈希桶(
table[index] = new Node(hash, key, value, null)); - 情况 2:哈希桶不为空,且头节点 key 与插入 key 相等:替换头节点的 value;
- 情况 3:哈希桶不为空,头节点 key 不相等:
- 若头节点是红黑树节点(TreeNode):调用红黑树的
putTreeVal()方法插入节点(按红黑树规则插入并维护平衡); - 若头节点是链表节点(Node):遍历链表,寻找 key 相等的节点(通过
key.equals()判断):- 找到:替换该节点的 value;
- 未找到:在链表尾部插入新 Node 节点(尾插法),插入后判断链表长度是否超过阈值(默认 8):
- 若超过:调用
treeifyBin()方法,将链表转为红黑树(需满足数组长度 >= 64,否则仅扩容)。
- 若超过:调用
- 若头节点是红黑树节点(TreeNode):调用红黑树的
3. 判断是否需要扩容
- 插入节点后,若 HashMap 实际元素个数(size)超过阈值(数组长度 × 扩容因子 0.75):调用
resize()方法扩容(数组长度翻倍)。
简化流程图
put(key, value)
→ 计算 hash(key) 和 index
→ table[index] == null → 插入新 Node
→ 否则,判断头节点 key 是否相等 → 是则替换 value
→ 否则,判断头节点类型(红黑树/链表)
→ 红黑树 → 插入红黑树
→ 链表 → 遍历链表,找到则替换 value,未找到则尾插新 Node
→ 链表长度>8 → 转红黑树(数组长度>=64)
→ size > 阈值 → resize() 扩容
举例说明
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1); // 哈希桶为空,直接插入
map.put("a", 2); // 头节点 key 相等,替换 value 为 2
map.put("b", 3); // 哈希冲突,链表尾插
// 多次插入冲突节点,链表长度>8 且数组长度>=64,转为红黑树
关键要点
- 核心逻辑:定位下标 → 处理冲突 → 插入节点 → 扩容;
- 冲突处理:链表(尾插)或红黑树;
- 扩容触发:size > 数组长度 × 0.75。
15、HashMap 的 get 方法逻辑?
核心概念
HashMap 的 get(Object key) 方法核心是“计算哈希值 → 定位数组下标 → 查找节点”,JDK 8 后的完整逻辑如下。
详细逻辑+举例
1. 计算 key 的哈希值和数组下标
- 若 key 为 null:哈希值为 0,下标为 0;
- 若 key 不为 null:通过
hash(key)方法计算哈希值; - 数组下标 =
(数组长度 - 1) & 哈希值。
2. 定位哈希桶,查找节点
获取数组下标对应的节点(table[index]),分三种情况:
- 情况 1:哈希桶为空(table[index] == null):返回 null(无对应 key);
- 情况 2:哈希桶不为空,且头节点 key 与查询 key 相等:返回头节点的 value;
- 情况 3:哈希桶不为空,且头节点 key 不相等:
- 若头节点是红黑树节点(TreeNode):调用红黑树的
getTreeNode()方法,按红黑树查找规则查找 key 对应的节点,找到返回 value,未找到返回 null; - 若头节点是链表节点(Node):遍历链表,逐个比较节点的 key(先比较哈希值,再通过
key.equals()比较):- 找到:返回该节点的 value;
- 未找到:返回 null。
- 若头节点是红黑树节点(TreeNode):调用红黑树的
简化流程图
get(key)
→ 计算 hash(key) 和 index
→ table[index] == null → 返回 null
→ 否则,判断头节点 key 是否相等 → 是则返回 value
→ 否则,判断头节点类型(红黑树/链表)
→ 红黑树 → 红黑树中查找 → 返回 value 或 null
→ 链表 → 遍历链表查找 → 返回 value 或 null
举例说明
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a")); // 1(头节点匹配)
System.out.println(map.get("c")); // null(无对应节点)
// 若 "b" 存储在红黑树中,get("b") 按红黑树规则查找
关键要点
- 平均查找时间复杂度 O(1)(哈希桶无冲突);
- 最坏情况 O(logn)(哈希桶为红黑树);
- 查找顺序:先匹配哈希值,再匹配
equals()(保证 key 唯一性)。
16、HashMap 是线程安全的吗?
核心概念
HashMap 不是线程安全的!多线程并发访问 HashMap 时,可能出现数据覆盖、链表环化、并发修改异常等问题。
线程安全问题+举例
-
数据覆盖
- 场景:多个线程同时执行
put操作,可能导致同一个哈希桶的节点被覆盖;
HashMap<String, Integer> map = new HashMap<>(); // 线程 1 new Thread(() -> map.put("a", 1)).start(); // 线程 2 new Thread(() -> map.put("a", 2)).start(); // 可能出现最终 map.get("a") 为 1 或 2,或数据丢失 - 场景:多个线程同时执行
-
链表环化(JDK 7)
- 场景:多线程同时扩容时,头插法可能导致链表形成环,后续遍历链表时陷入死循环;
- JDK 8 已通过尾插法修复,但 HashMap 仍线程不安全。
-
ConcurrentModificationException
- 场景:一个线程遍历 HashMap(如迭代器),另一个线程修改 HashMap(如
put/remove),触发 fail-fast 机制,抛出异常;
HashMap<String, Integer> map = new HashMap<>(); map.put("a", 1); // 遍历线程 new Thread(() -> { for (String key : map.keySet()) {} }).start(); // 修改线程 new Thread(() -> map.put("b", 2)).start(); // 可能抛 ConcurrentModificationException - 场景:一个线程遍历 HashMap(如迭代器),另一个线程修改 HashMap(如
-
数据不一致
- 场景:多线程同时读写,可能出现“读线程获取到旧数据”“统计 size 不准确”等问题。
线程安全替代方案
- 高并发场景:
ConcurrentHashMap(推荐,性能最优); - 低并发场景:
Collections.synchronizedMap(new HashMap<>()); - 旧代码维护:
Hashtable(不推荐新代码)。
关键要点
- 单线程/低并发(无修改)场景可使用 HashMap;
- 多线程修改场景必须使用线程安全替代方案;
- 避免在多线程中直接使用 HashMap,即使加锁也不如
ConcurrentHashMap高效。
17、HashMap 是怎么解决 hash 冲突的?
核心概念
Hash 冲突(哈希冲突)是指两个不同的对象,通过 hashCode() 计算出的哈希码值相同,HashMap 解决冲突的核心方式是“链地址法”,配合 JDK 8+ 红黑树优化。
解决方式+举例
1. 链地址法(核心)
- 原理:将哈希冲突的节点以“链表”形式存储在同一个哈希桶(数组下标)中;
- JDK 7:链表采用头插法(新节点插入链表头部);
- JDK 8:链表采用尾插法(新节点插入链表尾部),避免多线程扩容时的环化问题。
2. 红黑树优化(JDK 8+)
- 问题:链表过长时,查询时间复杂度从 O(1) 退化到 O(n);
- 优化:当链表长度超过阈值(默认 8),且数组长度 >= 64 时,链表自动转为红黑树;当链表长度小于阈值(默认 6)时,红黑树转回链表;
- 效果:红黑树查询时间复杂度为 O(logn),大幅提升长冲突链的查询效率。
3. 哈希值优化(减少冲突概率)
- JDK 8 中
hash(key)方法通过“高位异或低位”(key.hashCode() ^ (key.hashCode() >>> 16))优化哈希值分布,让哈希值的高位也参与下标计算,减少哈希冲突。
4. 动态扩容(降低冲突密度)
- 当 HashMap 中元素个数超过阈值(数组长度 × 扩容因子 0.75)时,数组长度翻倍(扩容),哈希桶数量增加,降低每个桶的节点数(冲突密度),间接减少哈希冲突。
举例说明(链地址法+红黑树)
HashMap<String, Integer> map = new HashMap<>();
// "a" 和 "b" 哈希冲突,存储在同一数组下标,形成链表
map.put("a", 1);
map.put("b", 2);
// 继续添加冲突节点,链表长度>8 且数组长度>=64,转为红黑树
map.put("c", 3);
// ... 继续添加 6 个冲突节点
关键要点
- 核心方案:链地址法(链表存储冲突节点);
- 优化方案:红黑树(解决长链表查询慢);
- 辅助方案:哈希值优化(减少冲突)、动态扩容(降低冲突密度);
- 其他冲突解决方法(HashMap 未使用):开放地址法、再哈希法、公共溢出区。
18、HashMap 是怎么扩容的?
核心概念
HashMap 的扩容(resize() 方法)是指“数组长度翻倍”,核心目的是增加哈希桶数量,降低哈希冲突密度,提升查询/插入性能,JDK 8 后的扩容逻辑如下。
详细扩容步骤+举例
1. 扩容触发条件
当 HashMap 实际元素个数(size)超过 阈值(threshold = 数组长度 × 扩容因子 loadFactor) 时,触发扩容(默认扩容因子 0.75)。
2. 扩容核心步骤
-
步骤 1:计算新容量和新阈值
- 若原数组长度为 0(初始化):新容量 = 初始容量(默认 16),新阈值 = 16 × 0.75 = 12;
- 若原数组长度 > 0:新容量 = 原容量 × 2,新阈值 = 新容量 × 0.75;
- 若原阈值超过 int 最大值(2^30):不再扩容,阈值设为 Integer.MAX_VALUE。
-
步骤 2:创建新数组
新数组长度为新容量(2 的幂)。 -
步骤 3:迁移原数组节点到新数组(JDK 8 优化点)
遍历原数组的每个哈希桶,将节点迁移到新数组,分三种情况:- 情况 1:哈希桶为空 → 跳过;
- 情况 2:哈希桶仅有一个节点 → 直接计算新下标(
(新容量 - 1) & 节点哈希值),放入新数组; - 情况 3:哈希桶为链表/红黑树:
- 链表:遍历链表,通过
(原容量 & 节点哈希值)判断节点新下标:- 结果为 0 → 留在原下标位置(新数组中对应原下标);
- 结果非 0 → 移动到新下标(原下标 + 原容量);
拆分链表为两个子链表,分别放入新数组的对应下标;
- 红黑树:按链表迁移逻辑拆分红黑树为两个子链表,若子链表长度 <= 6,转为普通链表;否则,转为红黑树。
- 链表:遍历链表,通过
-
步骤 4:更新 HashMap 状态
将table指向新数组,更新threshold为新阈值。
举例说明(扩容前后下标变化)
- 原容量 16,原下标 =
hash & 15; - 新容量 32,新下标 =
hash & 31; - 若
hash & 16 == 0→ 新下标 = 原下标; - 若
hash & 16 != 0→ 新下标 = 原下标 + 16。
关键要点
- JDK 8 扩容优化:通过
(原容量 & 节点哈希值)判断下标,无需重新计算哈希值,提升迁移效率; - 扩容后数组长度仍为 2 的幂(保证哈希分布均匀);
- 扩容阈值 = 数组长度 × 0.75(平衡空间利用率和查询性能)。
19、HashMap 如何实现同步?
核心概念
HashMap 本身是线程不安全的,实现同步(多线程安全访问)有 3 种常用方式,核心是通过“外部锁”或“线程安全的包装类”保证并发一致性。
同步方式+举例
1. 使用 Collections.synchronizedMap() 包装
- 原理:通过
Collections.synchronizedMap(new HashMap<>())返回一个线程安全的 Map 代理类,所有方法通过synchronized锁(锁对象为代理类实例)同步; - 优点:简单易用,可包装任意 HashMap 实现;
- 缺点:锁粒度大(对象级锁),并发性能一般;
- 示例:
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); syncMap.put("a", 1); Integer value = syncMap.get("a");
2. 使用 ConcurrentHashMap 替代
- 原理:JDK 8+ 中
ConcurrentHashMap采用 CAS + synchronized 锁(锁粒度为哈希桶),支持高并发,性能优于synchronizedMap; - 优点:并发性能优秀,支持原子操作(如
putIfAbsent); - 缺点:key/value 不允许为 null;
- 示例:
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>(); chm.put("a", 1); chm.putIfAbsent("b", 2); // 原子操作,线程安全
3. 手动加锁(synchronized 或 ReentrantLock)
- 原理:在多线程访问 HashMap 的代码块前后手动加锁,保证同一时间仅一个线程操作 HashMap;
- 优点:锁粒度可自定义(如按 key 加锁),灵活性高;
- 缺点:需手动管理锁的获取和释放,易出现死锁、漏解锁等问题;
- 示例(synchronized 加锁):
Map<String, Integer> map = new HashMap<>(); Object lock = new Object(); // 多线程操作时加锁 synchronized (lock) { map.put("a", 1); } synchronized (lock) { Integer value = map.get("a"); }
关键要点
- 高并发场景:优先使用
ConcurrentHashMap(性能最优); - 低并发场景:使用
Collections.synchronizedMap(简单易用); - 特殊场景(如自定义锁粒度):手动加锁(需谨慎实现);
- 避免使用
HashMap手动加锁(易出错,性能不如ConcurrentHashMap)。
20、HashMap 中的负载因子是什么?
核心概念
HashMap 中的负载因子(loadFactor)是指“HashMap 允许的最大填充比例”,核心作用是平衡空间利用率和查询性能。
详细解析+举例
1. 负载因子的定义
负载因子 = HashMap 实际元素个数(size) / 数组长度(capacity);
默认值为 0.75(JDK 源码中定义:static final float DEFAULT_LOAD_FACTOR = 0.75f)。
2. 负载因子与阈值的关系
阈值(threshold)= 数组长度 × 负载因子;
当 HashMap 的 size 超过阈值时,触发扩容(数组长度翻倍)。
3. 负载因子的影响(关键)
负载因子的大小直接影响 HashMap 的性能和内存占用:
- 负载因子过大(如 1.0):
- 优点:空间利用率高(数组填充更满);
- 缺点:哈希冲突概率大幅增加,链表/红黑树变长,查询/插入性能下降;
- 负载因子过小(如 0.5):
- 优点:哈希冲突少,查询/插入性能高;
- 缺点:空间利用率低(数组频繁扩容,大量哈希桶为空),内存浪费。
4. 为什么默认负载因子是 0.75?
JDK 选择 0.75 作为默认负载因子,是“空间利用率”和“查询性能”的折中:
- 统计学证明,0.75 是哈希表在“泊松分布”下的最优值,此时哈希冲突的概率最低;
- 若负载因子大于 0.75,哈希冲突概率会急剧上升;若小于 0.75,内存浪费明显。
举例说明
// 创建负载因子为 0.5 的 HashMap,初始容量 16
Map<String, Integer> map = new HashMap<>(16, 0.5f);
// 阈值 = 16 × 0.5 = 8
// 当 size > 8 时,触发扩容(数组长度变为 32)
map.put("a", 1);
// ... 继续添加 8 个元素,size 变为 9,触发扩容
关键要点
- 负载因子默认 0.75,平衡空间和性能;
- 自定义负载因子:内存充足、追求高性能 → 较小值(如 0.5);内存紧张、可接受略低性能 → 较大值(如 0.8);
- 负载因子仅影响扩容触发时机,不直接影响哈希冲突本身(冲突由哈希值和数组长度决定)。
21、Hashtable 为什么不叫 HashTable?
核心概念
Hashtable 不叫 HashTable 的核心原因是 Java 早期的命名规范不严格 + 历史遗留问题。
详细原因
-
Java 1.0 时代的命名随意性
- Hashtable 是 JDK 1.0 就存在的古老类,当时 Java 尚未形成严格的“驼峰命名规范”(如类名首字母大写,后续单词首字母大写);
- 同期的古老类(如
Vector、Stack)也未严格遵循驼峰规范,命名较为随意。
-
拼写错误的历史遗留
- 按现代驼峰规范,“哈希表”应命名为
HashTable(Hash+Table),但 Hashtable 被误写为Hashtable(少了一个大写T); - 由于 Java 对类名大小写敏感,且需向后兼容(已有代码依赖
Hashtable类名),该拼写错误被保留至今。
- 按现代驼峰规范,“哈希表”应命名为
-
与其他集合类的命名对比
- JDK 1.2 引入集合框架后,新增的
HashMap、TreeMap等类严格遵循驼峰规范; - Hashtable 因兼容性保留原有拼写,形成了命名差异。
- JDK 1.2 引入集合框架后,新增的
关键要点
- 核心原因:早期命名不规范 + 历史遗留,非设计意图;
- 注意:
Hashtable是正确类名,HashTable是错误拼写(编译报错); - 新代码优先使用
HashMap/ConcurrentHashMap,避免使用Hashtable。
22、ConcurrentHashMap 的数据结构?
核心概念
ConcurrentHashMap 的数据结构随 JDK 版本演进,核心是“线程安全 + 高性能”,JDK 7 和 JDK 8+ 差异显著。
详细结构+举例
1. JDK 7:分段锁(Segment 数组 + HashEntry 链表)
- 核心结构:
Segment数组:本质是可重入锁(ReentrantLock),数组长度默认 16,每个 Segment 对应一个哈希桶;HashEntry链表:每个 Segment 内部包含一个 HashEntry 数组,哈希冲突的节点以链表形式存储;
- 线程安全机制:通过“分段锁”实现,不同线程访问不同 Segment 时互不阻塞,支持并发写操作;
- 缺点:Segment 数组长度固定,扩容成本高,并发度受限于 Segment 数量。
2. JDK 8+:数组 + 链表/红黑树 + CAS + synchronized
- 核心结构:
- 数组(Node 数组):存储
Node节点,数组长度为 2 的幂,默认 16; - 链表/红黑树:哈希冲突时,节点以链表形式存储,链表长度>8 且数组长度>=64 时转为红黑树;
Node节点:存储 key、value、hash、next 指针,红黑树节点为TreeNode(继承 Node);
- 数组(Node 数组):存储
- 线程安全机制:
- 摒弃分段锁,改用“CAS + synchronized 锁”:synchronized 锁粒度为哈希桶(数组下标),仅锁定冲突的链表/红黑树,并发度大幅提升;
- 原子操作:通过 CAS 实现
putIfAbsent等原子方法,避免锁开销;
- 优点:锁粒度小、扩容灵活、并发性能优于 JDK 7,且支持红黑树优化,查询效率更高。
举例说明(JDK 8+ 结构)
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("a", 1);
chm.put("b", 2);
// 若 "a"、"b" 哈希冲突,存储在同一数组下标,形成链表;
// 多线程同时向不同下标插入节点,互不阻塞;
// 多线程向同一下标插入节点,通过 synchronized 锁序列化执行
关键要点
- JDK 8+ 核心优化:分段锁 → CAS + synchronized,红黑树优化查询;
- 线程安全核心:锁粒度从 Segment 级 → 哈希桶级;
- 性能优势:高并发场景下,读写性能远优于 Hashtable 和
Collections.synchronizedMap()。
23、ArrayList 是线程安全的么?
核心概念
ArrayList 不是线程安全的!多线程并发访问 ArrayList 时,可能出现数据覆盖、数组越界、并发修改异常等问题。
线程安全问题+举例
-
数据覆盖
- 场景:多个线程同时执行
add操作,可能导致元素被覆盖;
ArrayList<String> list = new ArrayList<>(); // 线程 1 new Thread(() -> list.add("a")).start(); // 线程 2 new Thread(() -> list.add("a")).start(); // 可能出现 list 大小为 1(数据覆盖) - 场景:多个线程同时执行
-
数组越界异常
- 场景:扩容时,一个线程正在扩容,另一个线程执行
add操作,可能因数组长度未更新导致ArrayIndexOutOfBoundsException;
- 场景:扩容时,一个线程正在扩容,另一个线程执行
-
ConcurrentModificationException
- 场景:一个线程遍历 ArrayList(如迭代器),另一个线程修改 ArrayList(
add/remove),触发 fail-fast 机制,抛出异常;
ArrayList<String> list = new ArrayList<>(); list.add("a"); // 遍历线程 new Thread(() -> { for (String s : list) {} }).start(); // 修改线程 new Thread(() -> list.add("b")).start(); // 可能抛 ConcurrentModificationException - 场景:一个线程遍历 ArrayList(如迭代器),另一个线程修改 ArrayList(
-
数据不一致
- 场景:多线程同时读写,可能出现“读线程获取到旧数据”“统计 size 不准确”等问题。
线程安全替代方案
CopyOnWriteArrayList:写时复制,读多写少场景推荐;Collections.synchronizedList(new ArrayList<>()):全局锁,低并发场景使用;- 手动加锁:通过
synchronized或ReentrantLock保护 ArrayList 操作。
关键要点
- 单线程场景优先使用 ArrayList(性能优);
- 多线程修改场景必须使用线程安全替代方案;
CopyOnWriteArrayList适合读多写少(如配置缓存),synchronizedList适合低并发读写。
24、常用的线程安全的 List 集合有哪些?
核心概念
常用的线程安全 List 有 3 种,核心区别在于实现机制和性能,分别是 Vector、Collections.synchronizedList()、CopyOnWriteArrayList。
详细对比+举例
| 特性 | Vector | Collections.synchronizedList(List) | CopyOnWriteArrayList |
|---|---|---|---|
| 实现方式 | 所有方法加 synchronized(对象级锁) |
包装普通 List,所有方法通过 synchronized 同步 |
写时复制(写操作复制新数组,读操作读原数组) |
| 性能 | 差(同一时间仅一个线程访问) | 一般(锁粒度大) | 读操作极快(无锁),写操作较慢(复制数组) |
| 迭代器类型 | 安全失败(fail-safe):遍历副本,修改不抛异常 | 快速失败(fail-fast):遍历中修改抛 ConcurrentModificationException |
安全失败(fail-safe):遍历原数组快照,修改不抛异常 |
| 适用场景 | 旧代码维护、低并发场景 | 低并发场景、需包装自定义 List | 读多写少场景(如配置缓存、日志收集) |
| 举例 | new Vector<>() |
Collections.synchronizedList(new ArrayList<>()) |
new CopyOnWriteArrayList<>() |
举例说明
- CopyOnWriteArrayList(读多写少推荐)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("a");
// 读操作无锁,高并发安全
String s = cowList.get(0);
// 写操作复制新数组,线程安全
cowList.add("b");
- Collections.synchronizedList(低并发)
List<String> arrayList = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(arrayList);
syncList.add("a");
// 所有操作加锁,线程安全
String s = syncList.get(0);
- Vector(旧代码)
Vector<String> vector = new Vector<>();
vector.add("a");
String s = vector.get(0);
// 所有方法加 synchronized,线程安全但性能差
关键要点
- 读多写少 →
CopyOnWriteArrayList(性能最优); - 低并发读写 →
Collections.synchronizedList(灵活); - 旧代码维护 →
Vector(不推荐新代码); CopyOnWriteArrayList适合读操作远多于写操作的场景(如系统配置、字典数据)。
25、循环删除 List 集合可能会发生什么异常?
核心概念
循环删除 List 时,若操作不当,最常见的异常是 ConcurrentModificationException(并发修改异常),还可能出现“元素漏删”“数组越界”等问题,核心原因是 List 的迭代器 fail-fast 机制和索引偏移。
异常场景+举例
1. 增强 for 循环(foreach)删除 → ConcurrentModificationException
- 场景:增强 for 循环底层使用
Iterator迭代器,迭代器的next()方法会检查modCount(修改次数)与expectedModCount(预期修改次数)是否一致,直接调用list.remove()会修改modCount,导致两者不一致,触发异常;
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // 抛 ConcurrentModificationException
}
}
2. 普通 for 循环按索引删除 → 元素漏删
- 场景:删除元素后,List 数组长度缩短,后续元素前移,索引
i递增导致跳过下一个元素;
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "b", "c"));
for (int i = 0; i < list.size(); i++) {
if ("b".equals(list.get(i))) {
list.remove(i); // 删除第一个 "b" 后,第二个 "b" 前移到索引 1,i 递增为 2,漏删
}
}
// 结果:list = ["a", "b", "c"](漏删一个 "b")
3. 普通 for 循环倒序删除 → 无异常(正确方式)
- 场景:倒序删除时,删除当前元素后,后续元素前移不影响前面的索引,不会漏删;
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "b", "c"));
for (int i = list.size() - 1; i >= 0; i--) {
if ("b".equals(list.get(i))) {
list.remove(i); // 无漏删
}
}
// 结果:list = ["a", "c"]
4. Iterator 迭代器删除 → 无异常(正确方式)
- 场景:迭代器的
remove()方法会同步expectedModCount与modCount,避免异常;
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("b".equals(s)) {
it.remove(); // 无异常
}
}
// 结果:list = ["a", "c"]
关键要点
- 禁止使用增强 for 循环删除 List 元素;
- 推荐使用:Iterator 迭代器删除 或 倒序 for 循环删除;
ConcurrentModificationException不是仅在多线程中出现,单线程遍历中修改也会触发。
26、ArrayList 和 LinkedList 的区别?
核心概念
ArrayList 和 LinkedList 是 List 的两个核心实现,核心区别在于底层数据结构,进而导致性能和适用场景差异。
详细对比+举例
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层实现 | 动态数组(数组扩容) | 双向链表(每个节点存储前驱/后继指针) |
| 随机访问 | 快(O(1),直接通过索引访问数组元素) | 慢(O(n),需遍历链表查找元素) |
| 增删操作 | 尾端增删快(O(1)),中间增删慢(O(n),需移动数组元素) | 任意位置增删快(O(1),仅需修改节点指针),查找元素慢(O(n)) |
| 内存占用 | 连续内存,浪费少(数组扩容预留空间) | 非连续内存,每个节点额外存储前后指针,内存开销大 |
| 线程安全 | 线程不安全 | 线程不安全 |
| 迭代器 | 支持随机访问迭代器(ListIterator) |
支持双向迭代器(可向前/向后遍历) |
| 适用场景 | 频繁查询、尾端增删(如列表展示、数据缓存) | 频繁中间增删、无需随机访问(如队列、栈) |
| 举例 | new ArrayList<>() 存储商品列表(频繁查询) |
new LinkedList<>() 实现消息队列(频繁入队出队) |
举例说明(性能差异)
// ArrayList 随机访问快
List<String> arrayList = new ArrayList<>();
arrayList.add("a");
String s1 = arrayList.get(0); // O(1)
// LinkedList 中间增删快
List<String> linkedList = new LinkedList<>();
linkedList.add("a");
linkedList.add(1, "b"); // O(1),仅修改指针
String s2 = linkedList.get(1); // O(n),需遍历
关键要点
- 随机访问/查询频繁 → ArrayList;
- 中间增删频繁 → LinkedList;
- 尾端增删场景两者性能差异不大,优先选 ArrayList(更节省内存);
- 避免在 LinkedList 中使用
get(index)进行随机访问(性能极差)。
27、ArrayList 和 Vector 的区别?
核心概念
ArrayList 和 Vector 都是基于动态数组的 List 实现,核心区别在于线程安全和性能。
详细对比+举例
| 特性 | ArrayList | Vector |
|---|---|---|
| 线程安全 | 线程不安全(多线程需手动同步) | 线程安全(所有方法加 synchronized 锁) |
| 性能 | 高(无锁开销) | 低(对象级锁,并发阻塞) |
| 扩容机制 | 初始容量 10,扩容为原容量的 1.5 倍 | 初始容量 10,默认扩容为原容量的 2 倍(可指定扩容因子) |
| 迭代器类型 | 快速失败(fail-fast):遍历中修改抛 ConcurrentModificationException |
安全失败(fail-safe):迭代器遍历副本,修改不抛异常 |
| API 丰富度 | 无 elements() 方法,API 简洁 |
支持 elements() 方法(返回 Enumeration 枚举器) |
| 适用场景 | 单线程/低并发场景(推荐) | 旧代码维护、需线程安全的低并发场景 |
| 举例 | new ArrayList<>() 存储用户列表 |
new Vector<>() 兼容 legacy 系统 |
举例说明(线程安全差异)
// ArrayList 线程不安全
ArrayList<String> arrayList = new ArrayList<>();
new Thread(() -> arrayList.add("a")).start();
new Thread(() -> arrayList.add("b")).start();
// 可能出现数据覆盖
// Vector 线程安全
Vector<String> vector = new Vector<>();
new Thread(() -> vector.add("a")).start();
new Thread(() -> vector.add("b")).start();
// 无数据覆盖(所有方法加锁)
关键要点
- 新代码优先选 ArrayList(性能优、内存效率高);
- 线程安全需求选
ConcurrentHashMap/CopyOnWriteArrayList,而非 Vector; - Vector 仅用于维护旧代码,不推荐新场景使用。
28、什么是 CopyOnWriteArrayList?
核心概念
CopyOnWriteArrayList 是 Java 提供的线程安全 List 实现,核心机制是“写时复制(Copy-On-Write)”,即读操作无锁,写操作(add/remove)时复制一份新数组,修改后替换原数组,保证读操作的线程安全。
核心原理+举例
1. 数据结构
- 底层存储一个 volatile 修饰的数组(
volatile Object[] array),volatile 保证数组引用的可见性; - 读操作直接读取原数组,无锁,性能极高;
- 写操作流程:
- 复制原数组为新数组(新数组长度=原长度+1);
- 在新数组中执行修改操作(如添加元素);
- 将
array引用指向新数组(volatile 保证其他线程可见);
- 写操作通过
ReentrantLock加锁,避免多个线程同时复制数组,保证写操作的原子性。
2. 优缺点
- 优点:
- 读操作无锁,支持高并发读;
- 迭代器为 fail-safe(遍历原数组快照,修改不会抛异常);
- 缺点:
- 写操作开销大(复制数组);
- 数据一致性弱(读操作可能获取到旧数据);
- 内存占用高(写操作时同时存在原数组和新数组)。
举例说明
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
// 写操作(加锁,复制新数组)
cowList.add("a");
cowList.add("b");
// 读操作(无锁,直接读原数组)
String s1 = cowList.get(0); // "a"
// 迭代器遍历(快照)
Iterator<String> it = cowList.iterator();
// 遍历中修改数组
cowList.add("c");
// 迭代器仍遍历原数组快照,输出 "a"、"b"
while (it.hasNext()) {
System.out.println(it.next());
}
关键要点
- 适用场景:读多写少(如配置缓存、日志收集);
- 不适用场景:写操作频繁、需强数据一致性;
- 核心优势:高并发读性能,无
ConcurrentModificationException。
);
29、什么是 fail-safe?
核心结论:fail-safe(安全失败)是集合迭代器的特性,迭代时遍历集合“快照”,修改原集合不抛异常,迭代器仍遍历快照数据。
详细解析+案例
实现原理
- 迭代器创建时,复制原集合数据(如数组副本);
- 迭代过程操作副本,原集合修改不影响副本;
- 迭代完成后,副本被 GC 回收,无并发修改异常。
支持 fail-safe 的集合
CopyOnWriteArrayList/CopyOnWriteArraySet:写时复制,迭代器遍历原数组快照;ConcurrentHashMap:迭代器遍历节点快照;Hashtable的Enumeration枚举器:遍历集合副本。
案例(CopyOnWriteArrayList 的 fail-safe)
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = list.iterator();
// 迭代中修改原集合
list.add("d");
// 迭代器遍历快照,输出 a、b、c(不包含 d)
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
关键要点
- 优点:迭代时修改集合无异常,线程安全;
- 缺点:数据一致性弱(可能读旧数据)、内存开销大(复制副本);
- 适用场景:读多写少,无需强实时一致性的场景。
30、什么是 fail-fast?
核心结论:fail-fast(快速失败)是集合迭代器的特性,迭代中修改集合(非迭代器自身 remove 方法),立即抛 ConcurrentModificationException,快速终止迭代。
详细解析+案例
实现原理
- 集合维护
modCount变量,记录修改次数(add/remove时自增); - 迭代器创建时,记录当前
modCount为expectedModCount; - 迭代中调用
next()时,校验modCount == expectedModCount:- 不等则抛
ConcurrentModificationException,避免遍历不一致数据。
- 不等则抛
支持 fail-fast 的集合
- 非线程安全集合:
ArrayList、LinkedList、HashMap、HashSet等; - 线程安全包装类:
Collections.synchronizedList包装的集合。
案例(ArrayList 的 fail-fast)
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = list.iterator();
// 迭代中直接修改原集合(非迭代器 remove 方法)
list.remove(1);
// 调用 next() 时抛 ConcurrentModificationException
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
关键要点
- 触发条件:迭代中通过集合自身方法修改(
add/remove),且未通过迭代器remove同步expectedModCount; - 目的:快速发现并发修改,避免遍历错误数据;
- 注意:单线程迭代中修改集合也会触发,非多线程专属。
31、fail-fast 与 fail-safe 有什么区别?
核心结论:两者核心差异在于迭代时是否允许修改集合、是否抛异常、数据一致性和性能开销。
详细对比+案例
| 特性 | fail-fast | fail-safe |
|---|---|---|
| 核心定义 | 迭代中修改集合,立即抛 ConcurrentModificationException |
迭代中修改集合,不抛异常,遍历快照 |
| 迭代对象 | 原集合 | 集合快照(副本) |
| 数据一致性 | 强(避免遍历不一致数据) | 弱(可能遍历旧数据) |
| 性能开销 | 低(无副本开销) | 高(需复制集合数据) |
| 支持集合 | 非线程安全集合(ArrayList、HashMap 等) | 线程安全集合(CopyOnWriteArrayList、ConcurrentHashMap 等) |
| 修改检测 | 校验 modCount == expectedModCount |
无检测,修改不影响快照 |
| 案例 | ArrayList 迭代中 list.remove() 抛异常 |
CopyOnWriteArrayList 迭代中 list.add() 正常执行 |
案例对比
// fail-fast 案例
List<String> fastList = new ArrayList<>(Arrays.asList("a"));
Iterator<String> fastIt = fastList.iterator();
fastList.add("b");
// fastIt.next(); // 抛 ConcurrentModificationException
// fail-safe 案例
List<String> safeList = new CopyOnWriteArrayList<>(Arrays.asList("a"));
Iterator<String> safeIt = safeList.iterator();
safeList.add("b");
safeIt.next(); // 正常返回 "a",无异常
32、HashSet 的底层实现原理是什么?
核心结论:HashSet 底层基于 HashMap 实现,利用 HashMap 的 key 唯一性实现去重,value 是固定空对象。
详细解析+案例
核心结构
- 内部持有
HashMap实例:private transient HashMap<E, Object> map; - 存储元素作为 HashMap 的 key,value 是固定空对象:
private static final Object PRESENT = new Object(); - 去重原理:依赖 HashMap 的 key 唯一性(相同 key 覆盖 value)。
核心方法实现(映射 HashMap 操作)
| HashSet 方法 | 底层 HashMap 操作 |
|---|---|
add(E e) |
map.put(e, PRESENT),返回 true(key 不存在)/false(key 已存在) |
remove(Object o) |
map.remove(o) == PRESENT,删除 key 并判断是否存在 |
contains(Object o) |
map.containsKey(o),判断 key 是否存在 |
size() |
map.size(),返回 key 数量 |
clear() |
map.clear(),清空所有 key-value |
案例(HashSet 去重)
HashSet<String> set = new HashSet<>();
set.add("a");
set.add("a"); // 重复元素,添加失败
set.add(null);
set.add(null); // 重复 null,添加失败
System.out.println(set.size()); // 2(元素:"a"、null)
关键要点
- 特性继承:无序性、线程不安全、支持
null元素(仅一个),与 HashMap 一致; - 去重判断:先比较
hashCode(),再比较equals(),两者都相等视为重复; - 性能:查询、添加、删除均为 O(1)(无哈希冲突时),冲突时退化为 O(logn)(红黑树)。
33、怎么确保一个集合不能被修改?
核心结论:通过“只读包装”“固定长度集合”“不可变集合框架”三种方式,禁止集合的添加、删除、修改操作。
详细实现+案例
1. 使用 Collections.unmodifiableXXX() 方法(推荐)
- 原理:包装原集合,返回只读代理类,所有修改方法(
add/remove/set)抛UnsupportedOperationException; - 优点:简单易用,支持所有集合类型(List/Set/Map);
- 缺点:原集合仍可修改,只读集合会同步变化(代理类持有原集合引用);
- 案例:
List<String> list = new ArrayList<>(Arrays.asList("a", "b")); List<String> unmodifiableList = Collections.unmodifiableList(list); // unmodifiableList.add("c"); // 抛 UnsupportedOperationException list.add("c"); // 原集合修改,只读集合同步变化(unmodifiableList 变为 ["a", "b", "c"])
2. 使用 Arrays.asList() 方法
- 原理:返回固定长度的内部类
ArrayList(非java.util.ArrayList),不支持add/remove,但支持set修改元素; - 优点:无需额外包装,直接创建;
- 缺点:仅支持 List,可修改元素值,长度固定;
- 案例:
List<String> fixedList = Arrays.asList("a", "b"); // fixedList.add("c"); // 抛 UnsupportedOperationException fixedList.set(0, "x"); // 允许修改元素,fixedList 变为 ["x", "b"]
3. 使用 Guava 的 ImmutableXXX 集合(强只读,推荐)
- 原理:Google Guava 提供
ImmutableList/ImmutableSet/ImmutableMap,创建时复制原集合数据,生成不可变副本,原集合修改不影响; - 优点:强只读(禁止所有修改,包括
set)、线程安全、数据一致性强; - 缺点:需引入 Guava 依赖;
- 案例:
// 引入依赖:com.google.guava:guava:32.0.0-jre List<String> list = new ArrayList<>(Arrays.asList("a", "b")); ImmutableList<String> immutableList = ImmutableList.copyOf(list); // immutableList.add("c"); // 编译报错(无 add 方法) // immutableList.set(0, "x"); // 编译报错(无 set 方法) list.add("c"); // 原集合修改,immutableList 仍为 ["a", "b"]
关键要点
- 简单场景、允许原集合同步变化 →
Collections.unmodifiableXXX(); - 仅需固定长度 List →
Arrays.asList(); - 强只读、高安全性 → Guava
ImmutableXXX集合; - 注意:只读集合仅限制集合自身修改,若集合元素是可变对象(如自定义类),仍可修改元素内部属性(需配合不可变对象使用)。
更多推荐



所有评论(0)