ArrayList与CopyOnWriteArrayList源码深度解析及面试拷打
在Java开发中,ArrayList
和CopyOnWriteArrayList
是常用的集合类,面试中常被用来考察候选人对集合框架的理解、源码实现以及并发场景的掌握。本文将深入剖析ArrayList
和CopyOnWriteArrayList
的源码,聚焦容易被面试官"拷打"的关键点,并延伸出更多值得深挖的内容,模拟面试场景,带你全面备战!
一、ArrayList源码解析
ArrayList
是基于动态数组实现的List
,支持随机访问,线程不安全。以下是源码中容易被拷打的重点。
1. 核心数据结构
-
底层实现 :
Object[] elementData
,动态数组存储元素。 -
容量与长度:
- 容量:
elementData.length
,数组分配的空间。 - 长度:
size
,实际存储的元素个数。
- 容量:
-
默认容量 :初始为0(延迟分配),首次添加元素时扩容到10(
DEFAULT_CAPACITY = 10
)。
源码片段(构造方法):
csharp
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组
}
拷打点:
-
Q1 :
ArrayList
初始容量是多少?首次添加元素会发生什么?- 答:初始容量为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
或数据不一致。 - 原因 :
add
、remove
等操作未加锁,迭代器依赖modCount
检测并发修改。
源码片段 (add
方法):
arduino
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
拷打点:
-
Q6 :为什么
ArrayList
迭代时抛出ConcurrentModificationException
?- 答 :迭代器记录了
expectedModCount
,若modCount
变化(因其他线程修改),则抛出异常。
- 答 :迭代器记录了
-
Q7 :如何让
ArrayList
线程安全?-
答:
- 使用
Collections.synchronizedList(new ArrayList<>())
。 - 使用
CopyOnWriteArrayList
。 - 手动加锁(如
synchronized
或ReentrantLock
)。
- 使用
-
4. Fail-Fast机制
- 定义 :
ArrayList
的迭代器采用快速失败机制,检测到并发修改立即抛出ConcurrentModificationException
。 - 实现 :通过
modCount
记录结构修改次数,迭代器检查modCount
是否变化。
拷打点:
-
Q8:以下代码会抛异常吗?
iniArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); for (Integer i : list) { list.remove(i); }
-
答 :会抛
ConcurrentModificationException
,因迭代中调用remove
修改了modCount
。 -
解决 :使用
Iterator
的remove
方法:iniIterator<Integer> it = list.iterator(); while (it.hasNext()) { it.next(); it.remove(); }
-
5. 序列化
- 特性 :
ArrayList
实现Serializable
,但elementData
声明为transient
,避免序列化空槽。 - 实现 :通过
writeObject
和readObject
自定义序列化,只序列化有效元素。
源码片段:
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 :为什么
elementData
是transient
?- 答 :
elementData
可能包含空槽(size < capacity
),序列化只存储有效元素,节省空间。
- 答 :
二、CopyOnWriteArrayList源码解析
CopyOnWriteArrayList
是JUC
包中的线程安全List
,适合"读多写少"场景。以下是源码重点。
1. 核心数据结构
- 底层实现 :
volatile Object[] array
,保证数组引用的可见性。 - 线程安全机制 :写操作(
add
、remove
)通过复制数组实现,读操作无锁。
源码片段:
csharp
private transient volatile Object[] array;
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
拷打点:
-
Q10 :为什么使用
volatile
?- 答:确保多线程下数组引用的可见性,避免读取到旧数组。
2. 写操作(Copy-On-Write)
-
机制 :写操作(
add
、set
、remove
)加ReentrantLock
,复制新数组,修改后更新引用。 -
步骤:
- 获取锁。
- 复制当前数组。
- 在新数组上修改。
- 更新
array
引用。 - 释放锁。
源码片段 (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的优缺点?
-
优点:
- 读操作无锁,高并发读性能优异。
- 迭代器支持"快照"语义,迭代期间不抛
ConcurrentModificationException
。
-
缺点:
- 写操作开销大,复制数组耗时且占用内存。
- 数据一致性弱,读操作可能看到旧数据(最终一致性)。
-
-
Q12:适合什么场景?
- 答:读多写少,如事件监听器列表、缓存数据。
3. 读操作
-
机制 :直接访问
array
,无锁,性能高。 -
源码片段 (
get
方法):arduinopublic E get(int index) { return elementAt(getArray(), index); }
拷打点:
-
Q13:为什么读操作无锁?
- 答 :
array
是volatile
,保证可见性;Copy-On-Write确保写操作不影响当前数组。
- 答 :
4. 迭代器(快照机制)
-
特性 :返回数组快照,迭代期间不受写操作影响,不抛
ConcurrentModificationException
。 -
源码片段 (
COWIterator
):inistatic final class COWIterator<E> implements ListIterator<E> { private final Object[] snapshot; COWIterator(Object[] elements, int initialCursor) { snapshot = elements; cursor = initialCursor; } }
拷打点:
-
Q14 :
CopyOnWriteArrayList
迭代器支持remove
吗?- 答 :不支持,调用
remove
抛出UnsupportedOperationException
,因迭代器基于快照,修改不影响原数组。
- 答 :不支持,调用
5. 内存占用
- 问题:写操作频繁时,复制数组导致内存占用高。
- 优化:尽量减少写操作,或在初始化时预分配足够容量。
拷打点:
-
Q15:如何降低内存占用?
-
答:
- 构造时指定初始容量:
new CopyOnWriteArrayList<>(Arrays.asList(array))
。 - 批量操作(如
addAll
)减少复制次数。
- 构造时指定初始容量:
-
三、延伸拷打点
以下是ArrayList
和CopyOnWriteArrayList
相关的更多高阶问题,面试官可能进一步深挖。
1. ArrayList vs. LinkedList
-
Q16 :
ArrayList
和LinkedList
的区别及适用场景?-
答:
-
ArrayList:基于数组,随机访问O(1),增删慢O(n)(需移动元素)。
-
LinkedList:基于双向链表,增删快O(1)(仅修改指针),随机访问慢O(n)。
-
场景:
ArrayList
:适合随机访问、遍历。LinkedList
:适合频繁插入、删除。
-
-
2. ArrayList的快速失败 vs. CopyOnWriteArrayList的快照
-
Q17:Fail-Fast和快照机制的区别?
-
答:
- Fail-Fast (
ArrayList
):检测并发修改,立即抛异常,适合单线程或严格一致性场景。 - 快照 (
CopyOnWriteArrayList
):迭代基于数组副本,写操作不影响迭代,适合高并发读。
- Fail-Fast (
-
3. 并发场景选择
-
Q18:并发场景下如何选择集合?
-
答:
- 读多写少 :
CopyOnWriteArrayList
。 - 写多 :
Collections.synchronizedList
或Vector
。 - 高并发读写 :
ConcurrentHashMap
(若需要Map)或自定义锁机制。
- 读多写少 :
-
4. 性能测试
-
Q19 :如何测试
ArrayList
和CopyOnWriteArrayList
的性能?-
答:
-
使用
JMHBenchmark
或手动计时,测试读写操作。 -
示例(伪代码):
iniList<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. 序列化与深拷贝
-
Q20 :
ArrayList
和CopyOnWriteArrayList
如何实现深拷贝?-
答:
-
ArrayList:通过序列化或手动复制元素:
sqlArrayList<Integer> copy = new ArrayList<>(); for (Integer i : list) copy.add(i != null ? new Integer(i) : null);
-
CopyOnWriteArrayList:类似,但需注意元素深拷贝:
iniCopyOnWriteArrayList<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
代码?-
答:
iniArrayList<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是核心。
- 适合读多写少的高并发场景。
-
拷打重点:扩容、线程安全、迭代器机制、性能优化。
面试准备建议
- 熟读源码 :掌握
ArrayList
的grow
、add
、iterator
和CopyOnWriteArrayList
的add
、get
、COWIterator
。 - 理解机制:扩容、Fail-Fast、Copy-On-Write的原理及适用场景。
- 警惕陷阱:并发修改、内存占用、迭代器误用。
- 实践验证:编写代码测试扩容、并发异常、快照行为。
- 性能意识:根据读写比例选择合适的集合类。
通过以上内容,你将能应对面试官对ArrayList
和CopyOnWriteArrayList
的"深度拷打"!如有更多问题,欢迎留言讨论!