1. 为什么需要动态数组?
在Java编程中,数组是最基础的数据结构之一。但原生数组有个致命缺陷:长度固定。一旦创建,就无法动态扩展或收缩。想象你正在开发一个用户管理系统,最初分配了100个用户的空间,但当用户增长到101个时,系统就会崩溃。这就是ArrayList诞生的背景。
ArrayList是Java集合框架中最常用的动态数组实现。它内部维护了一个Object[]数组,当容量不足时自动扩容(通常是1.5倍)。这种设计既保留了数组随机访问的高效性(O(1)时间复杂度),又提供了动态调整的灵活性。
实际开发中,90%需要数组的场景都会优先选择ArrayList。除非对内存有极端要求,否则固定长度的原生数组很少直接使用。
2. ArrayList核心实现原理
2.1 底层数据结构剖析
打开ArrayList源码,你会发现这个关键字段:
transient Object[] elementData;这就是存储数据的核心数组。transient关键字表示序列化时会忽略这个字段,ArrayList自定义了序列化逻辑来优化空间。
扩容机制是ArrayList最精妙的部分。当调用add()方法且当前size == elementData.length时触发:
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); }这里有个性能陷阱:频繁扩容会导致大量数组拷贝。初始化时如果能预估大小,建议使用带初始容量的构造函数:
List<String> list = new ArrayList<>(1000); // 直接分配1000容量2.2 线程安全问题
ArrayList不是线程安全的。一个经典错误场景:
List<String> list = new ArrayList<>(); // 线程A list.add("A"); // 线程B list.add("B");当多线程并发修改时,可能导致:
- 数据覆盖
- ArrayIndexOutOfBoundsException
- 扩容时数组状态不一致
解决方案:
- 使用Collections.synchronizedList包装
- 改用CopyOnWriteArrayList(读多写少场景)
- 在方法内部new ArrayList(线程隔离)
3. 必须掌握的API实战
3.1 基础CRUD操作
ArrayList<String> fruits = new ArrayList<>(); // 增 fruits.add("Apple"); // 尾部添加 fruits.add(0, "Banana"); // 指定位置插入 // 删 fruits.remove(0); // 按索引删除 fruits.remove("Apple"); // 按元素删除 // 改 fruits.set(0, "Orange"); // 替换指定位置元素 // 查 String first = fruits.get(0); boolean hasApple = fruits.contains("Apple");3.2 批量操作技巧
// 批量添加 fruits.addAll(Arrays.asList("Grape", "Peach")); // 批量删除(交集) fruits.removeAll(Arrays.asList("Grape", "Peach")); // 保留交集 fruits.retainAll(Arrays.asList("Apple", "Orange")); // 清空 fruits.clear();3.3 迭代器高级用法
// 基本迭代 Iterator<String> it = fruits.iterator(); while(it.hasNext()) { System.out.println(it.next()); } // 删除元素的安全方式 Iterator<String> it = fruits.iterator(); while(it.hasNext()) { if(it.next().equals("Apple")) { it.remove(); // 唯一线程安全的删除方式 } }4. 性能优化实战
4.1 初始化容量优化
测试对比:
// 不指定初始容量 long start = System.currentTimeMillis(); List<Integer> list1 = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { list1.add(i); } System.out.println("默认容量耗时:" + (System.currentTimeMillis() - start)); // 指定足够容量 start = System.currentTimeMillis(); List<Integer> list2 = new ArrayList<>(1000000); for (int i = 0; i < 1000000; i++) { list2.add(i); } System.out.println("预分配容量耗时:" + (System.currentTimeMillis() - start));实测结果可能相差50%以上!
4.2 遍历性能对比
测试三种遍历方式:
// 1. for循环 for(int i=0; i<list.size(); i++) { String s = list.get(i); } // 2. 增强for循环 for(String s : list) {} // 3. forEach+lambda list.forEach(s -> {});在ArrayList中:
- 传统for循环最快(直接数组访问)
- 增强for循环会生成Iterator对象
- forEach有lambda开销
4.3 空间优化技巧
ArrayList删除元素后不会自动缩容,需要手动trimToSize():
list.removeIf(s -> s.startsWith("A")); // 批量删除 list.trimToSize(); // 释放多余空间5. 常见坑点与解决方案
5.1 并发修改异常
List<String> list = new ArrayList<>(Arrays.asList("A","B","C")); for(String s : list) { if(s.equals("B")) { list.remove(s); // 抛出ConcurrentModificationException } }正确做法:
- 使用Iterator.remove()
- 使用CopyOnWriteArrayList
- 使用fori循环倒序删除
5.2 泛型类型擦除
List<Integer> intList = new ArrayList<>(); List rawList = intList; rawList.add("String"); // 编译通过,运行时报错解决方案:
- 避免使用原生类型
- 使用@SuppressWarnings("unchecked")要谨慎
- 考虑使用ImmutableList
5.3 自定义对象处理
class Person { String name; // 必须重写equals和hashCode! @Override public boolean equals(Object o) { if(this == o) return true; if(!(o instanceof Person)) return false; return name.equals(((Person)o).name); } } List<Person> people = new ArrayList<>(); people.add(new Person("Alice")); boolean contains = people.contains(new Person("Alice")); // 依赖equals实现6. 进阶应用场景
6.1 实现栈结构
class SimpleStack<E> { private ArrayList<E> list = new ArrayList<>(); public void push(E item) { list.add(item); } public E pop() { if(list.isEmpty()) throw new EmptyStackException(); return list.remove(list.size()-1); } }6.2 数据分页处理
public static <T> List<T> getPage(List<T> source, int page, int size) { int fromIndex = (page - 1) * size; if(fromIndex >= source.size()) return Collections.emptyList(); int toIndex = Math.min(fromIndex + size, source.size()); return source.subList(fromIndex, toIndex); }6.3 与Stream API结合
List<String> filtered = list.stream() .filter(s -> s.length() > 3) .sorted() .collect(Collectors.toCollection(ArrayList::new));7. 面试高频问题解析
7.1 ArrayList vs LinkedList
从四个维度对比:
- 随机访问:ArrayList O(1) vs LinkedList O(n)
- 头插删除:ArrayList O(n) vs LinkedList O(1)
- 内存占用:ArrayList更紧凑 vs LinkedList节点开销
- 迭代性能:ArrayList缓存友好 vs LinkedList指针跳转
7.2 扩容机制细节
- 默认初始容量:10
- 扩容公式:newCapacity = oldCapacity + (oldCapacity >> 1)
- 最大容量:Integer.MAX_VALUE - 8(部分VM保留头信息)
- 精确控制扩容:ensureCapacity(int minCapacity)
7.3 fail-fast机制
ArrayList迭代器通过modCount检测并发修改:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }这是快速失败(fail-fast)设计,强调尽早暴露错误。
8. 最佳实践总结
- 初始化:尽量预估容量,避免多次扩容
- 线程安全:多线程环境使用CopyOnWriteArrayList或同步包装
- 遍历删除:只使用Iterator.remove()
- 空间管理:大数据量删除后调用trimToSize()
- 性能敏感:优先用fori而不是迭代器
- API选择:
- contains()比indexOf()更语义化
- subList()返回的是视图,修改会影响原列表
- 版本兼容:注意JDK8和后续版本在stream处理上的优化差异
实际项目中,我曾用ArrayList处理过百万级数据导入。关键经验是:提前分批次处理,每批用固定容量的ArrayList,处理完立即释放。这比用单个超大ArrayList内存效率高30%以上。