Java 集合面试题详细解答

以下按题目顺序逐一解答,每个问题包含核心概念、举例说明和关键要点,兼顾理解与记忆。

1、说说常见的集合有哪些?

核心概念

Java 集合框架核心分为 Collection(存储单个元素)和 Map(存储键值对)两大体系,常见集合按功能分类如下:

详细分类+举例

一、Collection 接口(单元素存储)
  1. List(有序、可重复)

    • ArrayList:基于动态数组,查询快、增删慢(如 new ArrayList<>() 存储用户列表);
    • LinkedList:基于双向链表,增删快、查询慢(如 new LinkedList<>() 实现队列);
    • Vector:线程安全的动态数组(古老类,性能差,如 new Vector<>() 兼容旧系统);
    • CopyOnWriteArrayList:写时复制的线程安全 List(如高并发读场景存储配置项)。
  2. Set(无序、不可重复)

    • HashSet:基于 HashMap,查询快(如 new HashSet<>() 存储用户 ID 去重);
    • LinkedHashSet:基于 LinkedHashMap,有序(插入顺序)(如 new LinkedHashSet<>() 记录访问历史);
    • TreeSet:基于 TreeMap,自然排序(如 new TreeSet<>() 对成绩排序)。
  3. 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 接口(标记接口,无抽象方法)。

支持随机访问的集合+举例

  1. ArrayList:基于动态数组,索引直接映射数组下标(如 list.get(3) 快速获取第 4 个元素);
  2. Vector:动态数组实现,支持 vector.get(2) 随机访问;
  3. CopyOnWriteArrayList:数组底层,支持 cowList.get(5) 随机访问;
  4. ArrayDeque:数组实现的双端队列,支持 deque.get(1) 索引访问;
  5. 数组(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 实现倒序排序
举例说明
  1. Comparable 示例(自然排序)
// 实体类实现 Comparable,定义自然排序(按年龄升序)
class Person implements Comparable<Person> {
    private int age;
    @Override
    public int compareTo(Person o) {
        return this.age - o.age; // 自身年龄 - 参数年龄 = 升序
    }
}
// 使用:Collections.sort(personList) 自动按年龄排序
  1. 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()
举例说明
  1. Enumeration 遍历 Vector
Vector<String> vector = new Vector<>();
vector.add("a");
Enumeration<String> en = vector.elements();
while (en.hasMoreElements()) {
    System.out.println(en.nextElement()); // 仅遍历,无法删除
}
  1. 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 遍历);
  • Iteratorremove() 是唯一安全的集合遍历删除方式。

6、集合使用泛型有什么优点?

核心概念

泛型(Generic)为集合指定存储元素的类型,核心优点是类型安全、代码简洁、避免类型转换异常。

优点+举例

  1. 编译期类型安全:限制集合存储类型,禁止存入错误元素(编译报错)

    List<String> list = new ArrayList<>();
    list.add("a");
    // list.add(123); // 编译报错:类型不兼容,避免存入 Integer
    
  2. 消除强制类型转换:遍历无需手动转换,代码简洁

    // 无泛型:需强制转换
    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 类型
    
  3. 代码复用:一套泛型代码适配多种类型

    // 泛型方法:适配 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));
    
  4. 可读性提升:明确集合存储类型,语义清晰

    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 是“键值对映射”,强行继承会破坏接口语义一致性。

详细原因+举例

  1. 存储结构冲突

    • Collection 定义“单个元素”的操作规范(如 add(E e) 添加单个元素);
    • Map 定义“键值对”的操作规范(如 put(K key, V value) 添加键值对);
    • 若 Map 继承 Collection,需实现 add(E e),但 Map 存储的是键值对,无法适配“添加单个元素”的语义(如 map.add("a") 无法确定是 key 还是 value)。
  2. 遍历方式冲突

    • Collection 通过 Iterator 遍历单个元素;
    • Map 需遍历 key(keySet())、value(values())或键值对(entrySet()),遍历逻辑与 Collection 完全不同。
  3. 接口设计原则
    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 指多线程并发访问时,无需额外同步即可保证数据一致性,常用的有 HashtableConcurrentHashMapCollections.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<>())
举例说明
  1. ConcurrentHashMap(高并发推荐)
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("a", 1);
chm.putIfAbsent("b", 2); // 原子操作:不存在则插入
System.out.println(chm.get("b")); // 2
  1. 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
  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 排序(如按时间戳排序日志、范围查询)
举例说明
  1. 选 HashMap(无需有序)
// 缓存用户信息(ID-User),无需排序,追求查询快
Map<String, User> userCache = new HashMap<>();
userCache.put("1001", new User("张三"));
User user = userCache.get("1001"); // O(1) 快速查询
  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 点。

详细改变+举例

  1. 底层数据结构优化:数组 + 链表/红黑树

    • JDK 7:数组 + 链表(头插法);
    • JDK 8:数组 + 链表(尾插法)/ 红黑树(链表长度>8 且数组长度>=64 时转换);
    • 优化效果:查询时间复杂度从 O(n) 优化到 O(logn)。
  2. 链表插入方式:尾插法替代头插法

    • JDK 7:头插法(新节点插入链表头部),多线程扩容时可能导致链表环化(死循环);
    • JDK 8:尾插法(新节点插入链表尾部),避免多线程扩容时的死循环问题(虽 HashMap 仍线程不安全,但稳定性提升)。
  3. 哈希值计算优化

    • JDK 7:hash(key) 方法通过多次移位和异或计算哈希值;
    • JDK 8:hash(key) 方法简化为 key.hashCode() ^ (key.hashCode() >>> 16)(高位异或低位),减少哈希冲突,让哈希值分布更均匀。
  4. 扩容机制优化

    • JDK 7:扩容时需重新计算每个节点的哈希值和数组下标;
    • JDK 8:扩容时通过 (原数组长度 & 原哈希值) 判断节点是否需要移动(0 则留在原位置,非 0 则移动到新数组 原下标 + 原数组长度 位置),避免重新计算哈希值,提升扩容效率。
  5. 节点类优化

    • JDK 7:Entry<K, V> 类(存储 key、value、hash、next 指针);
    • JDK 8:Node<K, V> 类(替代 Entry),红黑树节点为 TreeNode<K, V>(继承 Node,增加红黑树相关属性:parent、left、right、red 等)。
举例说明(哈希值计算优化)
// 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,否则仅扩容)。
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。
简化流程图
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 时,可能出现数据覆盖、链表环化、并发修改异常等问题。

线程安全问题+举例

  1. 数据覆盖

    • 场景:多个线程同时执行 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,或数据丢失
    
  2. 链表环化(JDK 7)

    • 场景:多线程同时扩容时,头插法可能导致链表形成环,后续遍历链表时陷入死循环;
    • JDK 8 已通过尾插法修复,但 HashMap 仍线程不安全。
  3. 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
    
  4. 数据不一致

    • 场景:多线程同时读写,可能出现“读线程获取到旧数据”“统计 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 早期的命名规范不严格 + 历史遗留问题

详细原因

  1. Java 1.0 时代的命名随意性

    • Hashtable 是 JDK 1.0 就存在的古老类,当时 Java 尚未形成严格的“驼峰命名规范”(如类名首字母大写,后续单词首字母大写);
    • 同期的古老类(如 VectorStack)也未严格遵循驼峰规范,命名较为随意。
  2. 拼写错误的历史遗留

    • 按现代驼峰规范,“哈希表”应命名为 HashTableHash+Table),但 Hashtable 被误写为 Hashtable(少了一个大写 T);
    • 由于 Java 对类名大小写敏感,且需向后兼容(已有代码依赖 Hashtable 类名),该拼写错误被保留至今。
  3. 与其他集合类的命名对比

    • JDK 1.2 引入集合框架后,新增的 HashMapTreeMap 等类严格遵循驼峰规范;
    • Hashtable 因兼容性保留原有拼写,形成了命名差异。

关键要点

  • 核心原因:早期命名不规范 + 历史遗留,非设计意图;
  • 注意: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);
  • 线程安全机制
    • 摒弃分段锁,改用“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 时,可能出现数据覆盖、数组越界、并发修改异常等问题。

线程安全问题+举例

  1. 数据覆盖

    • 场景:多个线程同时执行 add 操作,可能导致元素被覆盖;
    ArrayList<String> list = new ArrayList<>();
    // 线程 1
    new Thread(() -> list.add("a")).start();
    // 线程 2
    new Thread(() -> list.add("a")).start();
    // 可能出现 list 大小为 1(数据覆盖)
    
  2. 数组越界异常

    • 场景:扩容时,一个线程正在扩容,另一个线程执行 add 操作,可能因数组长度未更新导致 ArrayIndexOutOfBoundsException
  3. 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
    
  4. 数据不一致

    • 场景:多线程同时读写,可能出现“读线程获取到旧数据”“统计 size 不准确”等问题。

线程安全替代方案

  • CopyOnWriteArrayList:写时复制,读多写少场景推荐;
  • Collections.synchronizedList(new ArrayList<>()):全局锁,低并发场景使用;
  • 手动加锁:通过 synchronizedReentrantLock 保护 ArrayList 操作。

关键要点

  • 单线程场景优先使用 ArrayList(性能优);
  • 多线程修改场景必须使用线程安全替代方案;
  • CopyOnWriteArrayList 适合读多写少(如配置缓存),synchronizedList 适合低并发读写。

24、常用的线程安全的 List 集合有哪些?

核心概念

常用的线程安全 List 有 3 种,核心区别在于实现机制和性能,分别是 VectorCollections.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<>()
举例说明
  1. CopyOnWriteArrayList(读多写少推荐)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("a");
// 读操作无锁,高并发安全
String s = cowList.get(0);
// 写操作复制新数组,线程安全
cowList.add("b");
  1. Collections.synchronizedList(低并发)
List<String> arrayList = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(arrayList);
syncList.add("a");
// 所有操作加锁,线程安全
String s = syncList.get(0);
  1. 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() 方法会同步 expectedModCountmodCount,避免异常;
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. 复制原数组为新数组(新数组长度=原长度+1);
    2. 在新数组中执行修改操作(如添加元素);
    3. 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:迭代器遍历节点快照;
  • HashtableEnumeration 枚举器:遍历集合副本。
案例(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 时自增);
  • 迭代器创建时,记录当前 modCountexpectedModCount
  • 迭代中调用 next() 时,校验 modCount == expectedModCount
    • 不等则抛 ConcurrentModificationException,避免遍历不一致数据。
支持 fail-fast 的集合
  • 非线程安全集合:ArrayListLinkedListHashMapHashSet 等;
  • 线程安全包装类: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 集合;
  • 注意:只读集合仅限制集合自身修改,若集合元素是可变对象(如自定义类),仍可修改元素内部属性(需配合不可变对象使用)。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐