ArrayList是Java集合框架中最常用的动态数组实现,它解决了传统数组长度固定的痛点。在实际项目中,我几乎每天都会和ArrayList打交道,特别是在处理不确定数据量的场景时。比如最近开发的一个电商促销系统,需要动态加载用户领取的优惠券列表,ArrayList的自动扩容特性就派上了大用场。
与传统数组不同,ArrayList的容量会随着元素添加自动增长。这背后其实是一个精妙的设计:初始创建时内部维护一个长度为10的Object[]数组(JDK8),当元素数量达到当前容量时,会自动创建一个原数组1.5倍大小的新数组(>>1位运算实现),然后将旧数据拷贝过去。这种设计在空间和时间效率上达到了很好的平衡。
重要提示:ArrayList的自动扩容会导致数组拷贝,在已知数据量的大致范围时,建议通过构造函数预先设置initialCapacity,比如new ArrayList(1000),可以避免频繁扩容带来的性能损耗。
打开ArrayList源码,可以看到这三个关键字段:
java复制transient Object[] elementData; // 实际存储元素的数组
private int size; // 当前元素数量
private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
elementData这个Object数组才是真正存储数据的地方,之所以用transient修饰,是因为ArrayList自定义了序列化逻辑来优化空间。size字段记录的是逻辑上的元素个数,而非数组长度,这点新手容易混淆。我曾在一次性能优化中误用length属性导致bug,这个教训让我深刻理解了size和capacity的区别。
当执行add()操作时,会先调用ensureCapacityInternal()方法检查容量:
java复制private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 并发修改计数器
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 触发扩容
}
真正的扩容发生在grow()方法中,关键代码:
java复制int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 计算新容量为1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
这个1.5倍的扩容策略是经过精心设计的:
add(E e)方法的执行路径:
这里有个性能陷阱:在列表中间插入元素时,System.arraycopy()会导致元素后移。比如在index=0处插入,需要移动所有现有元素。我曾用ArrayList处理10万级数据的头部插入,结果性能惨不忍睹,后来改用LinkedList才解决。
remove(int index)的核心逻辑:
java复制E oldValue = elementData(index); // 检查越界并获取旧值
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // 清除引用,帮助GC
注意最后一步将末尾置为null很重要,可以避免对象滞留。有次我们系统出现内存泄漏,就是因为自定义的ArrayList子类没有正确处理这个细节。
ArrayList实现了RandomAccess接口,这个标记接口表明支持快速随机访问。其get()方法直接通过数组下标访问:
java复制public E get(int index) {
rangeCheck(index); // 下标越界检查
return elementData(index); // 直接数组访问
}
这种O(1)复杂度的访问是ArrayList的最大优势。在开发分页查询时,这种特性特别有用。比如我们需要实现内存中对大数据集的分页:
java复制List<User> page = largeList.subList(fromIndex, toIndex);
ArrayList是线程不安全的,最常见的异常就是ConcurrentModificationException。这个异常通过modCount机制实现:
java复制final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在多线程环境下,推荐使用以下替代方案:
根据我的实战经验,使用ArrayList时要注意:
java复制// 已知要存储约5000个元素
List<Order> orders = new ArrayList<>(5120);
java复制// 比循环add效率高
list.addAll(Arrays.asList(data));
java复制// 安全遍历方式
for(User user : new ArrayList<>(userList)) {
if(condition) userList.remove(user);
}
选择依据主要看操作类型:
实际项目中,我通常先用ArrayList,只有确实需要频繁中间修改时才切到LinkedList。曾经做过测试,在10万数据量下:
虽然Vector是线程安全的,但因其同步开销大,现代Java开发中已很少使用。关键区别:
在最近的一个高并发项目中,我们测试发现:
ArrayList自定义了writeObject/readObject方法:
java复制private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject(); // 写入非transient字段
s.writeInt(size); // 只写入实际元素数量
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
}
这种设计避免了序列化整个数组,节省了空间。在分布式缓存场景下,这个优化能显著减少网络传输量。
通过modCount实现的快速失败(fail-fast)机制,能在并发修改时快速发现问题而不是继续执行可能出错的操作。这种设计哲学值得学习——宁可立即报错也不要产生不可预期的结果。
在内存中实现分页是ArrayList的典型应用:
java复制public <T> List<T> getPage(List<T> sourceList, int page, int pageSize) {
int fromIndex = (page - 1) * pageSize;
if(fromIndex >= sourceList.size()) {
return Collections.emptyList();
}
int toIndex = Math.min(fromIndex + pageSize, sourceList.size());
return sourceList.subList(fromIndex, toIndex);
}
处理CSV文件时,ArrayList的批量操作非常高效:
java复制List<String> lines = new ArrayList<>(10000);
try (BufferedReader br = new BufferedReader(new FileReader("data.csv"))) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
if(lines.size() >= 5000) {
processBatch(lines); // 批量处理
lines.clear();
}
}
if(!lines.isEmpty()) processBatch(lines);
}
这种批处理方式比逐行处理效率高5-10倍,特别是在配合适当初始容量时。