Java集合框架中List常见问题
Java集合框架中List常见基础面试题
考查点:
List的基础知识掌握情况
对应实现的区别
线程安全
使用场景
问题:
说下Vector和ArrayList、LinkedList联系和区别?分别的使用场景。
答案:
线程安全
ArrayList:底层是数组实现,线程不安全。查询和修改非常快,但增加和删除速度较慢。
LinkedList:底层是双向链表,线程不安全。查询和修改速度慢,但增加和删除速度快。
Vector:底层是数组实现,线程安全。操作时使用
synchronized
进行加锁。
使用场景
Vector:已经很少使用。
增加和删除场景多时,使用LinkedList。
查询和修改多时,使用ArrayList。
保证线程安全的ArrayList实现方式
在多线程环境中,直接使用ArrayList
可能会导致线程安全问题。为了保证线程安全,可以采用以下几种方式:
方式一:自定义同步包装类
可以创建一个自定义的线程安全包装类,根据具体业务需求,在add
、update
、remove
等方法上加锁。这种方式提供了最大的灵活性,但需要手动管理锁,可能会增加编程复杂度。
public class SyncArrayList<E> {private final ArrayList<E> list = new ArrayList<>();private final Object lock = new Object();public void add(E e) {synchronized (lock) {list.add(e);}}public E remove(int index) {synchronized (lock) {return list.remove(index);}}// 其他方法...
}
方式二:使用Collections.synchronizedList
可以使用Collections.synchronizedList
方法来包装一个ArrayList
,从而实现线程安全。这种方式简单易用,但所有的操作都需要通过集合的迭代器来进行,以保证线程安全。
List<String> syncList = Collections.synchronizedList(new ArrayList<String>());
方式三:使用CopyOnWriteArrayList
CopyOnWriteArrayList
是一种线程安全的变体,适用于读多写少的场景。每次修改操作都会复制整个底层数组,从而保证读操作的高性能。这种方式简单且线程安全,但可能会因为复制数组而影响性能。
List<String> cowList = new CopyOnWriteArrayList<>();
方式四:使用ReentrantLock
可以使用ReentrantLock
来手动管理锁,这种方式提供了比synchronized
更细粒度的锁定控制,可以根据具体需求灵活使用。
import java.util.concurrent.locks.ReentrantLock;public class LockArrayList<E> {private final ArrayList<E> list = new ArrayList<>();private final ReentrantLock lock = new ReentrantLock();public void add(E e) {lock.lock();try {list.add(e);} finally {lock.unlock();}}public E remove(int index) {lock.lock();try {return list.remove(index);} finally {lock.unlock();}}// 其他方法...
}
总结
选择哪种方式取决于具体的使用场景和性能需求。如果需要频繁的读写操作,CopyOnWriteArrayList
可能是一个不错的选择。如果需要更细粒度的控制,可以考虑使用ReentrantLock
。对于简单的线程安全需求,Collections.synchronizedList
提供了一个快速且方便的解决方案。
CopyOnWriteArrayList的掌握情况(追问系列)
考查点:
CopyOnWriteArrayList的实现原理
CopyOnWriteArrayList与Collections.synchronizedList的区别
CopyOnWriteArrayList的使用场景
CopyOnWriteArrayList的设计思想及其优缺点
问题:
如果回答到上面的点则继续问,没回到则问,了解CopyOnWriteArrayList吗?和Collections.synchronizedList实现线程安全有什么区别,使用场景是怎样的?
答案:
CopyOnWriteArrayList与Collections.synchronizedList的区别
CopyOnWriteArrayList:
执行修改操作(如
add
、set
、remove
)时,会复制一份新的数据,代价昂贵。修改完成后,将原来的集合指向新的集合完成操作。
使用
ReentrantLock
来保证不会多个线程同时修改。场景:适合读操作远大于写操作的场景(读操作是不需要加锁的,直接获取,但是删除和增加需要加锁,读多写少)。
Collections.synchronizedList:
线程安全的原因是因为几乎每个方法都是使用
synchronized
加同步锁。场景:写操作性能比CopyOnWriteArrayList好,但是读操作性能并不如CopyOnWriteArrayList。
CopyOnWriteArrayList的设计思想及其优缺点
设计思想:
读写分离 + 最终一致
读操作不需要加锁,直接读取数据。
写操作(如添加、删除、修改)时,复制一份新的数据,修改完成后将原集合指向新集合,保证最终一致性。
优点:
读操作高性能,不需要加锁。
写操作时通过复制数据,避免了多个线程同时修改数据的问题。
缺点:
内存占用问题,由于写时复制,内存里面同时存在两个对象占用的内存,如果对象大则容易发生YoungGC和FullGC。
写操作性能相对较低,因为需要复制数据。
使用场景
CopyOnWriteArrayList:
适用于读多写少的场景,如缓存、配置信息等。
适用于对并发写入要求不高,但读操作非常频繁的场景。
Collections.synchronizedList:
适用于写操作频繁,且对读操作性能要求不高的场景。
适用于需要细粒度控制锁的场景。
总结
CopyOnWriteArrayList和Collections.synchronizedList各有优缺点,选择哪种方式取决于具体的使用场景和性能需求。如果读操作远多于写操作,且对内存占用不敏感,可以选择CopyOnWriteArrayList。如果写操作频繁,且对读操作性能要求不高,可以选择Collections.synchronizedList。
ArrayList 扩容机制详解(含 JDK1.7 前后差异 + 核心源码剖析)
一、面试“一句话”总结
当元素个数达到当前容量上限时,ArrayList 会触发一次“1.5 倍扩容”:申请一块 1.5 倍大小的新数组,把旧数据
Arrays.copyOf
过去,再更新内部引用完成扩容。
⚠️ JDK 版本差异:
JDK1.7 之前:无参构造器直接给内部数组长度 10。
JDK1.7 及以后:无参构造器先给空数组(length=0),第一次 add 时才真正扩容到 10,实现“懒加载”。
二、扩容全过程(JDK1.8 源码)
核心方法链路:add(E)
→ ensureCapacityInternal(size+1)
→ grow(int minCapacity)
→ Arrays.copyOf
// 1. add 入口
public boolean add(E e) {ensureCapacityInternal(size + 1); // 确保容量足够elementData[size++] = e;return true;
}// 2. 计算所需最小容量
private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 空数组时minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取 10}if (minCapacity - elementData.length > 0)grow(minCapacity);
}// 3. 真正的扩容逻辑
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍if (newCapacity - minCapacity < 0) // 1.5 倍不够,直接用 minCapacitynewCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)// 防止溢出newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝
}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) throw new OutOfMemoryError(); // 溢出变负数return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
三、JDK1.7 前后差异速记表
表格
复制
版本 | 无参构造行为 | 初始容量 | 触发第一次扩容时机 |
---|---|---|---|
≤1.6 | 直接 new Object[10] | 10 | 第 11 次 add |
≥1.7(含 8+) | 空数组 Object[0] | 0 | 第 1 次 add → 扩到 10 |
四、为什么采用 1.5 倍而不是 2 倍?
折中策略:扩容太频繁会浪费 CPU;一次性扩太大又浪费内存。1.5 倍在大多数场景下能平衡 CPU 与内存开销。
五、面试加分点
提前避免扩容:若已知大概元素数量,可在构造时指定容量
new ArrayList<>(expectedSize)
,减少复制次数与 GC 压力。极端情况:当元素数量接近
Integer.MAX_VALUE
时,扩容可能溢出,源码用hugeCapacity
处理。
一句话收尾:
ArrayList 的扩容就是“懒加载 + 1.5 倍复制”,JDK1.7 以后把初始容量延迟到第一次插入,既省内存又保持高性能。