ArrayList与CopyOnWriteArrayList源码深度解析及面试拷打

ArrayList与CopyOnWriteArrayList源码深度解析及面试拷打

在Java开发中,ArrayListCopyOnWriteArrayList是常用的集合类,面试中常被用来考察候选人对集合框架的理解、源码实现以及并发场景的掌握。本文将深入剖析ArrayListCopyOnWriteArrayList的源码,聚焦容易被面试官"拷打"的关键点,并延伸出更多值得深挖的内容,模拟面试场景,带你全面备战!

一、ArrayList源码解析

ArrayList是基于动态数组实现的List,支持随机访问,线程不安全。以下是源码中容易被拷打的重点。

1. 核心数据结构

  • 底层实现Object[] elementData,动态数组存储元素。

  • 容量与长度

    • 容量:elementData.length,数组分配的空间。
    • 长度:size,实际存储的元素个数。
  • 默认容量 :初始为0(延迟分配),首次添加元素时扩容到10(DEFAULT_CAPACITY = 10)。

源码片段(构造方法):

csharp 复制代码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组
}

拷打点

  • Q1ArrayList初始容量是多少?首次添加元素会发生什么?

    • :初始容量为0,首次添加元素触发扩容,分配容量10的数组。
  • Q2:为什么延迟分配容量?

    • :节省内存,避免无元素时分配无用空间。

2. 扩容机制

  • 触发时机 :添加元素时,若size >= elementData.length,则扩容。
  • 扩容规则 :新容量为旧容量的1.5倍(newCapacity = oldCapacity + (oldCapacity >> 1))。
  • 实现 :调用Arrays.copyOf复制数组到新数组。

源码片段grow方法):

ini 复制代码
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

拷打点

  • Q3:扩容的时间复杂度是多少?

    • :O(n),因需复制整个数组。
  • Q4:如何避免频繁扩容?

    • :构造时指定初始容量,例如new ArrayList<>(100)
  • Q5:扩容因子为什么是1.5?

    • :1.5是折中选择,平衡内存使用和扩容频率(相比2倍更节省空间,相比1.2倍减少扩容次数)。

3. 线程不安全

  • 问题 :多线程下,ArrayList可能抛出ConcurrentModificationException或数据不一致。
  • 原因addremove等操作未加锁,迭代器依赖modCount检测并发修改。

源码片段add方法):

arduino 复制代码
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

拷打点

  • Q6 :为什么ArrayList迭代时抛出ConcurrentModificationException

    • :迭代器记录了expectedModCount,若modCount变化(因其他线程修改),则抛出异常。
  • Q7 :如何让ArrayList线程安全?

      1. 使用Collections.synchronizedList(new ArrayList<>())
      2. 使用CopyOnWriteArrayList
      3. 手动加锁(如synchronizedReentrantLock)。

4. Fail-Fast机制

  • 定义ArrayList的迭代器采用快速失败机制,检测到并发修改立即抛出ConcurrentModificationException
  • 实现 :通过modCount记录结构修改次数,迭代器检查modCount是否变化。

拷打点

  • Q8:以下代码会抛异常吗?

    ini 复制代码
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    for (Integer i : list) {
        list.remove(i);
    }
    • :会抛ConcurrentModificationException,因迭代中调用remove修改了modCount

    • 解决 :使用Iteratorremove方法:

      ini 复制代码
      Iterator<Integer> it = list.iterator();
      while (it.hasNext()) {
          it.next();
          it.remove();
      }

5. 序列化

  • 特性ArrayList实现Serializable,但elementData声明为transient,避免序列化空槽。
  • 实现 :通过writeObjectreadObject自定义序列化,只序列化有效元素。

源码片段

ini 复制代码
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

拷打点

  • Q9 :为什么elementDatatransient

    • elementData可能包含空槽(size < capacity),序列化只存储有效元素,节省空间。

二、CopyOnWriteArrayList源码解析

CopyOnWriteArrayListJUC包中的线程安全List,适合"读多写少"场景。以下是源码重点。

1. 核心数据结构

  • 底层实现volatile Object[] array,保证数组引用的可见性。
  • 线程安全机制 :写操作(addremove)通过复制数组实现,读操作无锁。

源码片段

csharp 复制代码
private transient volatile Object[] array;
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

拷打点

  • Q10 :为什么使用volatile

    • :确保多线程下数组引用的可见性,避免读取到旧数组。

2. 写操作(Copy-On-Write)

  • 机制 :写操作(addsetremove)加ReentrantLock,复制新数组,修改后更新引用。

  • 步骤

    1. 获取锁。
    2. 复制当前数组。
    3. 在新数组上修改。
    4. 更新array引用。
    5. 释放锁。

源码片段add方法):

ini 复制代码
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        Object[] newElements = Arrays.copyOf(es, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }
}

拷打点

  • Q11:Copy-On-Write的优缺点?

    • 优点

      1. 读操作无锁,高并发读性能优异。
      2. 迭代器支持"快照"语义,迭代期间不抛ConcurrentModificationException
    • 缺点

      1. 写操作开销大,复制数组耗时且占用内存。
      2. 数据一致性弱,读操作可能看到旧数据(最终一致性)。
  • Q12:适合什么场景?

    • :读多写少,如事件监听器列表、缓存数据。

3. 读操作

  • 机制 :直接访问array,无锁,性能高。

  • 源码片段get方法):

    arduino 复制代码
    public E get(int index) {
        return elementAt(getArray(), index);
    }

拷打点

  • Q13:为什么读操作无锁?

    • arrayvolatile,保证可见性;Copy-On-Write确保写操作不影响当前数组。

4. 迭代器(快照机制)

  • 特性 :返回数组快照,迭代期间不受写操作影响,不抛ConcurrentModificationException

  • 源码片段COWIterator):

    ini 复制代码
    static final class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot;
        COWIterator(Object[] elements, int initialCursor) {
            snapshot = elements;
            cursor = initialCursor;
        }
    }

拷打点

  • Q14CopyOnWriteArrayList迭代器支持remove吗?

    • :不支持,调用remove抛出UnsupportedOperationException,因迭代器基于快照,修改不影响原数组。

5. 内存占用

  • 问题:写操作频繁时,复制数组导致内存占用高。
  • 优化:尽量减少写操作,或在初始化时预分配足够容量。

拷打点

  • Q15:如何降低内存占用?

      1. 构造时指定初始容量:new CopyOnWriteArrayList<>(Arrays.asList(array))
      2. 批量操作(如addAll)减少复制次数。

三、延伸拷打点

以下是ArrayListCopyOnWriteArrayList相关的更多高阶问题,面试官可能进一步深挖。

1. ArrayList vs. LinkedList

  • Q16ArrayListLinkedList的区别及适用场景?

      • ArrayList:基于数组,随机访问O(1),增删慢O(n)(需移动元素)。

      • LinkedList:基于双向链表,增删快O(1)(仅修改指针),随机访问慢O(n)。

      • 场景

        • ArrayList:适合随机访问、遍历。
        • LinkedList:适合频繁插入、删除。

2. ArrayList的快速失败 vs. CopyOnWriteArrayList的快照

  • Q17:Fail-Fast和快照机制的区别?

      • Fail-FastArrayList):检测并发修改,立即抛异常,适合单线程或严格一致性场景。
      • 快照CopyOnWriteArrayList):迭代基于数组副本,写操作不影响迭代,适合高并发读。

3. 并发场景选择

  • Q18:并发场景下如何选择集合?

      • 读多写少CopyOnWriteArrayList
      • 写多Collections.synchronizedListVector
      • 高并发读写ConcurrentHashMap(若需要Map)或自定义锁机制。

4. 性能测试

  • Q19 :如何测试ArrayListCopyOnWriteArrayList的性能?

      • 使用JMHBenchmark或手动计时,测试读写操作。

      • 示例(伪代码):

        ini 复制代码
        List<Integer> list = new CopyOnWriteArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) list.add(i);
        System.out.println("Add time: " + (System.nanoTime() - start));
      • 关注读写比例、线程数对性能的影响。

5. 序列化与深拷贝

  • Q20ArrayListCopyOnWriteArrayList如何实现深拷贝?

      • ArrayList:通过序列化或手动复制元素:

        sql 复制代码
        ArrayList<Integer> copy = new ArrayList<>();
        for (Integer i : list) copy.add(i != null ? new Integer(i) : null);
      • CopyOnWriteArrayList:类似,但需注意元素深拷贝:

        ini 复制代码
        CopyOnWriteArrayList<Integer> copy = new CopyOnWriteArrayList<>(list);

四、模拟面试场景

场景:面试官拿出一段代码,步步紧逼。

代码

ini 复制代码
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
for (Integer i : list) {
    list.add(i * 2);
}
  • Q21:代码会抛异常吗?

    • :会抛ConcurrentModificationException,因迭代中修改modCount
  • Q22 :改用CopyOnWriteArrayList会怎样?

    • :不会抛异常,迭代基于快照,新增元素不影响迭代,但结果可能不符合预期(新元素未反映在迭代中)。
  • Q23 :如何修复ArrayList代码?

    • ini 复制代码
      ArrayList<Integer> list = new ArrayList<>();
      list.add(1);
      list.add(2);
      ArrayList<Integer> temp = new ArrayList<>();
      for (Integer i : list) {
          temp.add(i * 2);
      }
      list.addAll(temp);

五、总结与建议

总结

  • ArrayList

    • 基于动态数组,随机访问高效,线程不安全。
    • 扩容机制(1.5倍)、Fail-Fast迭代器、序列化优化是重点。
    • 适合单线程、读多场景。
  • CopyOnWriteArrayList

    • 基于Copy-On-Write,读无锁,写复制数组。
    • 快照迭代器、volatile数组、ReentrantLock是核心。
    • 适合读多写少的高并发场景。
  • 拷打重点:扩容、线程安全、迭代器机制、性能优化。

面试准备建议

  1. 熟读源码 :掌握ArrayListgrowadditeratorCopyOnWriteArrayListaddgetCOWIterator
  2. 理解机制:扩容、Fail-Fast、Copy-On-Write的原理及适用场景。
  3. 警惕陷阱:并发修改、内存占用、迭代器误用。
  4. 实践验证:编写代码测试扩容、并发异常、快照行为。
  5. 性能意识:根据读写比例选择合适的集合类。

通过以上内容,你将能应对面试官对ArrayListCopyOnWriteArrayList的"深度拷打"!如有更多问题,欢迎留言讨论!

相关推荐
西京刀客1 小时前
Go多服务项目结构优化:为何每个服务单独设置internal目录?
开发语言·后端·golang
李匠20242 小时前
C++GO语言微服务之gorm框架操作MySQL
开发语言·c++·后端·golang
源码云商2 小时前
基于Spring Boot + Vue的高校心理教育辅导系统
java·spring boot·后端
黄俊懿4 小时前
【深入理解SpringCloud微服务】手写实现一个微服务分布式事务组件
java·分布式·后端·spring·spring cloud·微服务·架构师
Themberfue4 小时前
RabbitMQ ②-工作模式
开发语言·分布式·后端·rabbitmq
有梦想的攻城狮4 小时前
spring中的@Inject注解详情
java·后端·spring·inject
曼岛_5 小时前
[架构之美]Spring Boot多环境5种方案实现Dev/Test/Prod环境隔离
spring boot·后端·架构
Top`5 小时前
服务预热原理
java·后端·spring
幽络源小助理5 小时前
SpringBoot框架开发网络安全科普系统开发实现
java·spring boot·后端·spring·web安全
酷小洋6 小时前
JavaWeb基础
后端·web