每日Java集合面试系列(1):基础篇(Collection与Map的区别、Set接口如何保证元素唯一性、fail-fast与fail-safe的区别、ArrayList相关问题)

每日一句:要去做那些正确的事,而不是容易的事儿

系列介绍

今天是Java集合的第一篇。

"Java集合"系列!本系列旨在帮助Java开发者系统性地准备面试,每天精选至少10道经典面试题,涵盖Java集合、进阶、框架等各方面知识。坚持学习21天,助你面试通关!
基础面试题:

Java集合

1. Collection 和 Map 接口的区别是什么?

核心区别:数据结构与元素组织方式

  • Collection:存储单元素集合,包含三大子接口:
    • List:有序可重复(索引访问)
    • Set:无序唯一(基于 hashCode/equals
    • Queue:队列结构(FIFO/优先级)
  • Map:存储键值对(Key-Value)映射,Key 唯一(基于 hashCode/equals),Value 可重复。
  • 关键设计差异
    • Collection 使用 add()/remove() 操作元素;Map 使用 put()/get() 操作键值对。
    • Map 不是 Collection 的子接口,提供独立的 keySet()values()entrySet() 视图。
  • 哲学差异
    Collection 是线性/集合抽象,Map 是关联数组抽象(如字典)。两者解决不同领域问题,故设计为独立接口。

2. List、Set、Queue 的区别是什么?

特性ListSetQueue
顺序插入有序(支持索引)无序(LinkedHashSet/TreeSet除外)按规则排序(FIFO/优先级)
唯一性允许重复元素元素唯一(hashCode/equals可重复(LinkedList除外)
Null 支持允许多个 null最多一个 null(TreeSet 不允许)部分实现允许(如LinkedList)
典型实现ArrayList, LinkedListHashSet, TreeSetLinkedList, PriorityQueue
用途序列化数据存储去重集合任务调度/消息传递

3. Set 接口是如何保证元素唯一性的?

底层机制:hashCode()equals() 协同

  1. 添加元素流程
    • 调用元素的 hashCode() 计算哈希值 → 定位桶位置(Bucket)。
    • 若桶为空:直接存入。
    • 若桶非空:遍历桶内元素,调用 equals() 逐一同新元素比较。
      • 存在相等元素(equals() 返回 true):拒绝添加。
      • 无相等元素:存入桶中(链表或红黑树)。
  2. 实现依赖
    • HashSet 基于 HashMap(元素作 Key,固定 Object 作 Value)。
    • TreeSet 基于 TreeMap(通过 ComparatorComparable 排序去重)。
  3. 开发者契约
    必须正确重写 hashCode()equals()(相同对象必须有相同哈希值,否则破坏 Set 唯一性)。

4. 为什么 Collection 不直接继承 Cloneable 和 Serializable?

设计哲学:接口最小化原则

  • 职责分离
    Collection 接口专注定义集合操作(增删查改),克隆与序列化是实现细节,不应强制所有集合支持。
  • 灵活性
    • 不是所有集合需要克隆(如只读集合)。
    • 序列化可能涉及安全限制(如敏感数据集合)。
  • 实践方案
    具体实现类按需实现接口:
    • ArrayList 实现 SerializableCloneable
    • 自定义集合可选择不实现。
  • 结论
    避免“接口污染”,保持 Collection 简洁性,符合高内聚低耦合原则。

5. Iterator 和 List  Iterator 的区别?

能力IteratorListIterator
遍历方向单向(仅 next()双向(next() / previous()
遍历范围所有 Collection 实现仅 List 及其实现类
元素修改支持 remove()支持 add() / set() / remove()
索引访问不支持支持 nextIndex() / previousIndex()
并发安全均非线程安全

代码示例:

ListIterator<String> listIter = list.listIterator();
while (listIter.hasNext()) {
  String item = listIter.next();
  listIter.set(item + "_modified"); // 修改当前元素
  listIter.add("inserted");         // 插入新元素
}

6. fail-fast 和 fail-safe 机制的区别?

特性fail-fastfail-safe
工作原理直接操作原集合,迭代时检查修改操作集合副本/快照
修改检测通过 modCount 校验(expectedModCount != modCount无修改检查
异常行为抛出 ConcurrentModificationException不抛异常,继续迭代
数据一致性强一致(实时反映修改)弱一致(迭代开始后修改不可见)
实现类ArrayList, HashMap, VectorCopyOnWriteArrayList, ConcurrentHashMap
适用场景单线程环境高并发读多写少场景

7. ArrayList 的扩容机制是怎样的?默认初始容量是多少?

默认初始容量:10
扩容流程(以添加元素为例)

  1. 检查当前容量:if (size == elementData.length)
  2. 计算新容量:int newCapacity = oldCapacity + (oldCapacity >> 1)(即 1.5 倍)。
  3. 处理边界:
    • 最小容量需求:若 newCapacity < minCapacity(如一次添加多个元素),则 newCapacity = minCapacity
    • 最大容量限制:若 newCapacity > MAX_ARRAY_SIZEInteger.MAX_VALUE - 8),调用 hugeCapacity() 处理(可能扩容至 Integer.MAX_VALUE)。
  4. 数据迁移:Arrays.copyOf(elementData, newCapacity)耗时操作)。

示例
初始容量 10 → 满后扩容至 15 → 再满后扩容至 22(15*1.5)。


8. LinkedList 为什么可以用作队列(Queue)?

结构特性与接口实现

  1. 双向链表结构
    Node<E> 节点包含前驱/后继指针,支持 O(1) 时间复杂度的头尾操作。
  2. 实现 Queue 接口
    直接提供队列操作:
    • 队尾插入:add(E e) / offer(E e)
    • 队头移除:remove() / poll()
    • 查看队头:element() / peek()
  3. 对比数组队列
    无需扩容复制,无容量限制(基于链表动态增长),但内存开销更大(每个元素含指针)。

9. Vector 为什么是线程安全的?它的锁粒度是怎样的?

线程安全机制:方法级 synchronized 锁

  • 锁粒度:在方法声明添加 synchronized 关键字(如 public synchronized boolean add(E e)),相当于锁定 整个 Vector 实例(对象锁)。
  • 问题
    粗粒度锁导致高并发场景下性能低下(即使独立操作如 get(0)get(1) 也需串行执行)。
  • 替代方案
    • Collections.synchronizedList():提供同步包装器(锁粒度同 Vector)。
    • CopyOnWriteArrayList:写时复制(读无锁,写加锁)。
    • ConcurrentLinkedQueue:CAS 无锁队列。

10. ArrayList 和 CopyOnWriteArrayList 的区别?

维度ArrayListCopyOnWriteArrayList
线程安全非线程安全线程安全
锁机制无锁(快速失败)写操作加 ReentrantLock
数据存储动态数组写时复制(每次修改创建新数组)
迭代器行为fail-fast(检测修改抛异常)fail-safe(基于初始数组快照)
读性能O(1) 随机访问(极快)O(1) 随机访问(无锁)
写性能O(1) 均摊(尾部插入)O(n) 复制开销(写慢)
适用场景单线程 / 读多写少(需同步控制)高并发读,极少写(如监听器列表)

11. 为什么 LinkedList 的随机访问性能差?时间复杂度是多少?

根本原因:链表物理非连续存储

  • 访问流程
    需从链表头(或尾)遍历至目标索引位置:
    Node<E> node(int index) {
      if (index < (size >> 1)) { // 前半部分:从头遍历
        Node<E> x = first;
        for (int i = 0; i < index; i++) x = x.next;
      } else { // 后半部分:从尾遍历
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) x = x.prev;
      }
      return x;
    }
    
  • 时间复杂度
    • 平均 O(n):最坏情况需遍历整个链表(如访问末尾元素)。
    • 对比 ArrayList:O(1) 通过索引直接寻址。
  • 优化建议
    频繁随机访问用 ArrayList;频繁插入删除用 LinkedList。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值